Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yaoai-video
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
姚珂
yaoai-video
Commits
b7185a90
Commit
b7185a90
authored
Apr 26, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
分镜页面优化和运营后台-api2
parent
21e68a93
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
79 changed files
with
2969 additions
and
0 deletions
+2969
-0
admin-deployment.yaml
yaoai-comic-studio/k8s/templates/admin-deployment.yaml
+63
-0
admin-ingress.yaml
yaoai-comic-studio/k8s/templates/admin-ingress.yaml
+36
-0
admin-service.yaml
yaoai-comic-studio/k8s/templates/admin-service.yaml
+19
-0
admin-web-deployment.yaml
yaoai-comic-studio/k8s/templates/admin-web-deployment.yaml
+47
-0
admin-web-ingress.yaml
yaoai-comic-studio/k8s/templates/admin-web-ingress.yaml
+36
-0
admin-web-service.yaml
yaoai-comic-studio/k8s/templates/admin-web-service.yaml
+19
-0
AdminModuleConfig.java
...dmin/src/main/java/com/yaoai/admin/AdminModuleConfig.java
+9
-0
AdminUserAdminController.java
...a/com/yaoai/admin/adminuser/AdminUserAdminController.java
+54
-0
AdminUserAdminService.java
...java/com/yaoai/admin/adminuser/AdminUserAdminService.java
+137
-0
AdminUserCreateRequest.java
...com/yaoai/admin/adminuser/dto/AdminUserCreateRequest.java
+17
-0
AdminUserQuery.java
...in/java/com/yaoai/admin/adminuser/dto/AdminUserQuery.java
+17
-0
AdminUserUpdateRequest.java
...com/yaoai/admin/adminuser/dto/AdminUserUpdateRequest.java
+15
-0
AdminAuditLogController.java
...n/java/com/yaoai/admin/audit/AdminAuditLogController.java
+26
-0
AdminAuditQueryService.java
...in/java/com/yaoai/admin/audit/AdminAuditQueryService.java
+57
-0
AdminAuditService.java
...rc/main/java/com/yaoai/admin/audit/AdminAuditService.java
+17
-0
AdminAudited.java
...n/java/com/yaoai/admin/audit/annotation/AdminAudited.java
+17
-0
AdminAuditAspect.java
...n/java/com/yaoai/admin/audit/aspect/AdminAuditAspect.java
+0
-0
AuditLogQuery.java
...rc/main/java/com/yaoai/admin/audit/dto/AuditLogQuery.java
+26
-0
AdminAuthController.java
...c/main/java/com/yaoai/admin/auth/AdminAuthController.java
+71
-0
AdminAuthService.java
.../src/main/java/com/yaoai/admin/auth/AdminAuthService.java
+77
-0
AdminPermissionInterface.java
...n/java/com/yaoai/admin/auth/AdminPermissionInterface.java
+33
-0
StpAdminUtil.java
...dmin/src/main/java/com/yaoai/admin/auth/StpAdminUtil.java
+49
-0
BillingAdminController.java
.../java/com/yaoai/admin/billing/BillingAdminController.java
+79
-0
BillingAdminService.java
...ain/java/com/yaoai/admin/billing/BillingAdminService.java
+0
-0
BillingCostItem.java
...ain/java/com/yaoai/admin/billing/dto/BillingCostItem.java
+17
-0
BillingCostUpdateRequest.java
...com/yaoai/admin/billing/dto/BillingCostUpdateRequest.java
+12
-0
BillingRecordQuery.java
.../java/com/yaoai/admin/billing/dto/BillingRecordQuery.java
+28
-0
BillingSummaryItem.java
.../java/com/yaoai/admin/billing/dto/BillingSummaryItem.java
+27
-0
UsageDetailResponse.java
...java/com/yaoai/admin/billing/dto/UsageDetailResponse.java
+20
-0
PageResult.java
...dmin/src/main/java/com/yaoai/admin/common/PageResult.java
+19
-0
AdminGlobalExceptionHandler.java
...a/com/yaoai/admin/config/AdminGlobalExceptionHandler.java
+66
-0
SaTokenAdminConfig.java
.../main/java/com/yaoai/admin/config/SaTokenAdminConfig.java
+18
-0
DashboardController.java
...n/java/com/yaoai/admin/dashboard/DashboardController.java
+44
-0
DashboardService.java
...main/java/com/yaoai/admin/dashboard/DashboardService.java
+184
-0
DashboardOverview.java
...java/com/yaoai/admin/dashboard/dto/DashboardOverview.java
+23
-0
DashboardProviderStats.java
...com/yaoai/admin/dashboard/dto/DashboardProviderStats.java
+27
-0
DashboardTaskStats.java
...ava/com/yaoai/admin/dashboard/dto/DashboardTaskStats.java
+19
-0
DashboardTrendPoint.java
...va/com/yaoai/admin/dashboard/dto/DashboardTrendPoint.java
+20
-0
PlanAdminController.java
...c/main/java/com/yaoai/admin/plan/PlanAdminController.java
+38
-0
PlanAdminService.java
.../src/main/java/com/yaoai/admin/plan/PlanAdminService.java
+46
-0
PlanUpdateRequest.java
...main/java/com/yaoai/admin/plan/dto/PlanUpdateRequest.java
+25
-0
TenantAdminController.java
...in/java/com/yaoai/admin/tenant/TenantAdminController.java
+126
-0
TenantAdminService.java
.../main/java/com/yaoai/admin/tenant/TenantAdminService.java
+0
-0
TenantBalanceOverview.java
...ava/com/yaoai/admin/tenant/dto/TenantBalanceOverview.java
+17
-0
TenantDetail.java
...rc/main/java/com/yaoai/admin/tenant/dto/TenantDetail.java
+22
-0
TenantImpersonateRequest.java
.../com/yaoai/admin/tenant/dto/TenantImpersonateRequest.java
+9
-0
TenantImpersonateResponse.java
...com/yaoai/admin/tenant/dto/TenantImpersonateResponse.java
+17
-0
TenantListItem.java
.../main/java/com/yaoai/admin/tenant/dto/TenantListItem.java
+30
-0
TenantPlanChangeRequest.java
...a/com/yaoai/admin/tenant/dto/TenantPlanChangeRequest.java
+9
-0
TenantQuotaUpdateRequest.java
.../com/yaoai/admin/tenant/dto/TenantQuotaUpdateRequest.java
+21
-0
TenantRechargeRequest.java
...ava/com/yaoai/admin/tenant/dto/TenantRechargeRequest.java
+15
-0
TenantSuspendRequest.java
...java/com/yaoai/admin/tenant/dto/TenantSuspendRequest.java
+9
-0
TenantUpdateRequest.java
.../java/com/yaoai/admin/tenant/dto/TenantUpdateRequest.java
+11
-0
UserAdminController.java
...c/main/java/com/yaoai/admin/user/UserAdminController.java
+62
-0
UserAdminService.java
.../src/main/java/com/yaoai/admin/user/UserAdminService.java
+106
-0
ResetPasswordResponse.java
.../java/com/yaoai/admin/user/dto/ResetPasswordResponse.java
+11
-0
UserQuery.java
...min/src/main/java/com/yaoai/admin/user/dto/UserQuery.java
+17
-0
UserStatusRequest.java
...main/java/com/yaoai/admin/user/dto/UserStatusRequest.java
+9
-0
SeedancePromptBuilder.java
...va/com/yaoai/ai/providers/util/SeedancePromptBuilder.java
+142
-0
StructuredVideoGenerateRequest.java
.../com/yaoai/api/dto/ai/StructuredVideoGenerateRequest.java
+68
-0
UserPrompt.java
...ai-api/src/main/java/com/yaoai/api/dto/ai/UserPrompt.java
+39
-0
BillingChargeRequest.java
...main/java/com/yaoai/billing/dto/BillingChargeRequest.java
+48
-0
BillingCostService.java
...in/java/com/yaoai/billing/service/BillingCostService.java
+102
-0
V13__admin_backend.sql
...ap/src/main/resources/db/migration/V13__admin_backend.sql
+121
-0
V14__system_settings.sql
.../src/main/resources/db/migration/V14__system_settings.sql
+9
-0
V15__extend_ai_tasks_structured.sql
...esources/db/migration/V15__extend_ai_tasks_structured.sql
+24
-0
V16__multi_character_per_storyboard.sql
...rces/db/migration/V16__multi_character_per_storyboard.sql
+23
-0
Result.java
...-common/src/main/java/com/yaoai/common/result/Result.java
+43
-0
AdminAuditLog.java
.../src/main/java/com/yaoai/domain/entity/AdminAuditLog.java
+47
-0
AdminUser.java
...main/src/main/java/com/yaoai/domain/entity/AdminUser.java
+45
-0
Plan.java
...ai-domain/src/main/java/com/yaoai/domain/entity/Plan.java
+44
-0
SystemSetting.java
.../src/main/java/com/yaoai/domain/entity/SystemSetting.java
+24
-0
TenantQuota.java
...in/src/main/java/com/yaoai/domain/entity/TenantQuota.java
+33
-0
AdminAuditLogMapper.java
...ain/java/com/yaoai/domain/mapper/AdminAuditLogMapper.java
+11
-0
AdminUserMapper.java
...rc/main/java/com/yaoai/domain/mapper/AdminUserMapper.java
+11
-0
PlanMapper.java
...ain/src/main/java/com/yaoai/domain/mapper/PlanMapper.java
+11
-0
SystemSettingMapper.java
...ain/java/com/yaoai/domain/mapper/SystemSettingMapper.java
+17
-0
TenantQuotaMapper.java
.../main/java/com/yaoai/domain/mapper/TenantQuotaMapper.java
+11
-0
StructuredVideoSubmission.java
...ava/com/yaoai/pipeline/dto/StructuredVideoSubmission.java
+51
-0
No files found.
yaoai-comic-studio/k8s/templates/admin-deployment.yaml
0 → 100644
View file @
b7185a90
{{
- if .Values.adminServer.enabled
}}
apiVersion
:
apps/v1
kind
:
Deployment
metadata
:
name
:
{{
include "yaoai.fullname" .
}}
-admin
labels
:
{{
- include "yaoai.labels" . | nindent 4
}}
app.kubernetes.io/component
:
admin-server
spec
:
replicas
:
{{
.Values.adminServer.replicaCount
}}
selector
:
matchLabels
:
{{
- include "yaoai.selectorLabels" . | nindent 6
}}
app.kubernetes.io/component
:
admin-server
strategy
:
type
:
RollingUpdate
rollingUpdate
:
maxUnavailable
:
0
maxSurge
:
1
template
:
metadata
:
labels
:
{{
- include "yaoai.selectorLabels" . | nindent 8
}}
app.kubernetes.io/component
:
admin-server
annotations
:
{{
- toYaml .Values.podAnnotations | nindent 8
}}
spec
:
{{
- with .Values.imagePullSecrets
}}
imagePullSecrets
:
{{
- toYaml . | nindent 8
}}
{{
- end
}}
terminationGracePeriodSeconds
:
60
containers
:
-
name
:
admin-server
image
:
"
{{
.Values.adminServer.image.repository
}}:{{
.Values.adminServer.image.tag
}}"
imagePullPolicy
:
{{
.Values.adminServer.image.pullPolicy
}}
ports
:
-
name
:
http-admin
containerPort
:
8081
protocol
:
TCP
env
:
{{
- range $key
,
$val
:
= .Values.adminServer.env
}}
-
name
:
{{
$key
}}
value
:
{{
$val | quote
}}
{{
- end
}}
{{
- range $key
:
= keys .Values.secrets
}}
-
name
:
{{
$key
}}
valueFrom
:
secretKeyRef
:
name
:
{{
include "yaoai.fullname" $
}}
-secrets
key
:
{{
$key
}}
{{
- end
}}
livenessProbe
:
{{
- toYaml .Values.adminServer.livenessProbe | nindent 12
}}
readinessProbe
:
{{
- toYaml .Values.adminServer.readinessProbe | nindent 12
}}
resources
:
{{
- toYaml .Values.adminServer.resources | nindent 12
}}
lifecycle
:
preStop
:
exec
:
command
:
[
"
/bin/sh"
,
"
-c"
,
"
sleep
10"
]
{{
- end
}}
yaoai-comic-studio/k8s/templates/admin-ingress.yaml
0 → 100644
View file @
b7185a90
{{
- if and .Values.adminServer.enabled .Values.adminServer.ingress.enabled
}}
apiVersion
:
networking.k8s.io/v1
kind
:
Ingress
metadata
:
name
:
{{
include "yaoai.fullname" .
}}
-admin
labels
:
{{
- include "yaoai.labels" . | nindent 4
}}
app.kubernetes.io/component
:
admin-server
{{
- with .Values.adminServer.ingress.annotations
}}
annotations
:
{{
- toYaml . | nindent 4
}}
{{
- end
}}
spec
:
{{
- if .Values.adminServer.ingress.className
}}
ingressClassName
:
{{
.Values.adminServer.ingress.className
}}
{{
- end
}}
{{
- if .Values.adminServer.ingress.tls
}}
tls
:
{{
- toYaml .Values.adminServer.ingress.tls | nindent 4
}}
{{
- end
}}
rules
:
{{
- range .Values.adminServer.ingress.hosts
}}
-
host
:
{{
.host | quote
}}
http
:
paths
:
{{
- range .paths
}}
-
path
:
{{
.path
}}
pathType
:
{{
.pathType
}}
backend
:
service
:
name
:
{{
include "yaoai.fullname" $
}}
-admin
port
:
number
:
{{
$.Values.adminServer.service.port
}}
{{
- end
}}
{{
- end
}}
{{
- end
}}
yaoai-comic-studio/k8s/templates/admin-service.yaml
0 → 100644
View file @
b7185a90
{{
- if .Values.adminServer.enabled
}}
apiVersion
:
v1
kind
:
Service
metadata
:
name
:
{{
include "yaoai.fullname" .
}}
-admin
labels
:
{{
- include "yaoai.labels" . | nindent 4
}}
app.kubernetes.io/component
:
admin-server
spec
:
type
:
{{
.Values.adminServer.service.type
}}
selector
:
{{
- include "yaoai.selectorLabels" . | nindent 4
}}
app.kubernetes.io/component
:
admin-server
ports
:
-
name
:
http-admin
port
:
{{
.Values.adminServer.service.port
}}
targetPort
:
http-admin
protocol
:
TCP
{{
- end
}}
yaoai-comic-studio/k8s/templates/admin-web-deployment.yaml
0 → 100644
View file @
b7185a90
{{
- if .Values.adminWeb.enabled
}}
apiVersion
:
apps/v1
kind
:
Deployment
metadata
:
name
:
{{
include "yaoai.fullname" .
}}
-admin-web
labels
:
{{
- include "yaoai.labels" . | nindent 4
}}
app.kubernetes.io/component
:
admin-web
spec
:
replicas
:
{{
.Values.adminWeb.replicaCount
}}
selector
:
matchLabels
:
{{
- include "yaoai.selectorLabels" . | nindent 6
}}
app.kubernetes.io/component
:
admin-web
strategy
:
type
:
RollingUpdate
rollingUpdate
:
maxUnavailable
:
0
maxSurge
:
1
template
:
metadata
:
labels
:
{{
- include "yaoai.selectorLabels" . | nindent 8
}}
app.kubernetes.io/component
:
admin-web
spec
:
{{
- with .Values.imagePullSecrets
}}
imagePullSecrets
:
{{
- toYaml . | nindent 8
}}
{{
- end
}}
containers
:
-
name
:
admin-web
image
:
"
{{
.Values.adminWeb.image.repository
}}:{{
.Values.adminWeb.image.tag
}}"
imagePullPolicy
:
{{
.Values.adminWeb.image.pullPolicy
}}
ports
:
-
name
:
http
containerPort
:
80
protocol
:
TCP
env
:
-
name
:
ADMIN_API_UPSTREAM
value
:
"
http://{{
include
"yaoai.fullname" . }}-admin:{{ .Values.adminServer.service.port }}"
livenessProbe
:
{{
- toYaml .Values.adminWeb.livenessProbe | nindent 12
}}
readinessProbe
:
{{
- toYaml .Values.adminWeb.readinessProbe | nindent 12
}}
resources
:
{{
- toYaml .Values.adminWeb.resources | nindent 12
}}
{{
- end
}}
yaoai-comic-studio/k8s/templates/admin-web-ingress.yaml
0 → 100644
View file @
b7185a90
{{
- if and .Values.adminWeb.enabled .Values.adminWeb.ingress.enabled
}}
apiVersion
:
networking.k8s.io/v1
kind
:
Ingress
metadata
:
name
:
{{
include "yaoai.fullname" .
}}
-admin-web
labels
:
{{
- include "yaoai.labels" . | nindent 4
}}
app.kubernetes.io/component
:
admin-web
{{
- with .Values.adminWeb.ingress.annotations
}}
annotations
:
{{
- toYaml . | nindent 4
}}
{{
- end
}}
spec
:
{{
- if .Values.adminWeb.ingress.className
}}
ingressClassName
:
{{
.Values.adminWeb.ingress.className
}}
{{
- end
}}
{{
- if .Values.adminWeb.ingress.tls
}}
tls
:
{{
- toYaml .Values.adminWeb.ingress.tls | nindent 4
}}
{{
- end
}}
rules
:
{{
- range .Values.adminWeb.ingress.hosts
}}
-
host
:
{{
.host | quote
}}
http
:
paths
:
{{
- range .paths
}}
-
path
:
{{
.path
}}
pathType
:
{{
.pathType
}}
backend
:
service
:
name
:
{{
include "yaoai.fullname" $
}}
-admin-web
port
:
number
:
{{
$.Values.adminWeb.service.port
}}
{{
- end
}}
{{
- end
}}
{{
- end
}}
yaoai-comic-studio/k8s/templates/admin-web-service.yaml
0 → 100644
View file @
b7185a90
{{
- if .Values.adminWeb.enabled
}}
apiVersion
:
v1
kind
:
Service
metadata
:
name
:
{{
include "yaoai.fullname" .
}}
-admin-web
labels
:
{{
- include "yaoai.labels" . | nindent 4
}}
app.kubernetes.io/component
:
admin-web
spec
:
type
:
{{
.Values.adminWeb.service.type
}}
selector
:
{{
- include "yaoai.selectorLabels" . | nindent 4
}}
app.kubernetes.io/component
:
admin-web
ports
:
-
name
:
http
port
:
{{
.Values.adminWeb.service.port
}}
targetPort
:
http
protocol
:
TCP
{{
- end
}}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/AdminModuleConfig.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
;
import
org.springframework.context.annotation.ComponentScan
;
import
org.springframework.context.annotation.Configuration
;
@Configuration
@ComponentScan
(
"com.yaoai.admin"
)
public
class
AdminModuleConfig
{
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/adminuser/AdminUserAdminController.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
adminuser
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
cn.dev33.satoken.annotation.SaCheckRole
;
import
com.yaoai.admin.adminuser.dto.AdminUserCreateRequest
;
import
com.yaoai.admin.adminuser.dto.AdminUserQuery
;
import
com.yaoai.admin.adminuser.dto.AdminUserUpdateRequest
;
import
com.yaoai.admin.audit.annotation.AdminAudited
;
import
com.yaoai.admin.common.PageResult
;
import
com.yaoai.admin.user.dto.ResetPasswordResponse
;
import
com.yaoai.common.result.Result
;
import
com.yaoai.domain.entity.AdminUser
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.ModelAttribute
;
import
org.springframework.web.bind.annotation.PatchMapping
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.PostMapping
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
@RestController
@RequestMapping
(
"/admin-api/admin-users"
)
@RequiredArgsConstructor
@SaCheckLogin
(
type
=
"admin"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
})
public
class
AdminUserAdminController
{
private
final
AdminUserAdminService
adminUserAdminService
;
@GetMapping
public
Result
<
PageResult
<
AdminUser
>>
list
(
@ModelAttribute
AdminUserQuery
query
)
{
return
Result
.
ok
(
adminUserAdminService
.
list
(
query
));
}
@PostMapping
@AdminAudited
(
action
=
"ADMIN_USER_CREATE"
,
resource
=
"ADMIN_USER"
,
resourceIdParam
=
"username"
)
public
Result
<
AdminUser
>
create
(
@RequestBody
AdminUserCreateRequest
request
)
{
return
Result
.
ok
(
adminUserAdminService
.
create
(
request
));
}
@PatchMapping
(
"/{id}"
)
@AdminAudited
(
action
=
"ADMIN_USER_UPDATE"
,
resource
=
"ADMIN_USER"
,
resourceIdParam
=
"id"
)
public
Result
<
AdminUser
>
update
(
@PathVariable
Long
id
,
@RequestBody
AdminUserUpdateRequest
request
)
{
return
Result
.
ok
(
adminUserAdminService
.
update
(
id
,
request
));
}
@PostMapping
(
"/{id}/reset-password"
)
@AdminAudited
(
action
=
"ADMIN_USER_RESET_PASSWORD"
,
resource
=
"ADMIN_USER"
,
resourceIdParam
=
"id"
)
public
Result
<
ResetPasswordResponse
>
resetPassword
(
@PathVariable
Long
id
)
{
return
Result
.
ok
(
adminUserAdminService
.
resetPassword
(
id
));
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/adminuser/AdminUserAdminService.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
adminuser
;
import
cn.hutool.core.util.RandomUtil
;
import
cn.hutool.crypto.digest.BCrypt
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.core.toolkit.Wrappers
;
import
com.yaoai.admin.adminuser.dto.AdminUserCreateRequest
;
import
com.yaoai.admin.adminuser.dto.AdminUserQuery
;
import
com.yaoai.admin.adminuser.dto.AdminUserUpdateRequest
;
import
com.yaoai.admin.auth.StpAdminUtil
;
import
com.yaoai.admin.common.PageResult
;
import
com.yaoai.admin.user.dto.ResetPasswordResponse
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.AdminUser
;
import
com.yaoai.domain.mapper.AdminUserMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
org.springframework.util.StringUtils
;
import
java.time.LocalDateTime
;
import
java.util.Set
;
@Service
@RequiredArgsConstructor
public
class
AdminUserAdminService
{
private
static
final
Set
<
String
>
ROLES
=
Set
.
of
(
"SUPER_ADMIN"
,
"OPERATOR"
,
"FINANCE"
,
"READONLY"
);
private
final
AdminUserMapper
adminUserMapper
;
public
PageResult
<
AdminUser
>
list
(
AdminUserQuery
query
)
{
long
page
=
Math
.
max
(
1
,
query
.
getPage
());
long
size
=
Math
.
max
(
1
,
Math
.
min
(
query
.
getSize
(),
100
));
long
offset
=
(
page
-
1
)
*
size
;
long
total
=
adminUserMapper
.
selectCount
(
buildQuery
(
query
));
return
new
PageResult
<>(
adminUserMapper
.
selectList
(
buildQuery
(
query
)
.
orderByDesc
(
AdminUser:
:
getCreatedAt
)
.
last
(
"LIMIT "
+
size
+
" OFFSET "
+
offset
)),
total
,
page
,
size
);
}
@Transactional
public
AdminUser
create
(
AdminUserCreateRequest
request
)
{
if
(!
StringUtils
.
hasText
(
request
.
getUsername
()))
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"用户名不能为空"
);
}
if
(!
ROLES
.
contains
(
request
.
getRole
()))
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"运营角色无效"
);
}
if
(
adminUserMapper
.
selectCount
(
Wrappers
.<
AdminUser
>
lambdaQuery
()
.
eq
(
AdminUser:
:
getUsername
,
request
.
getUsername
()))
>
0
)
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"用户名已存在"
);
}
String
password
=
StringUtils
.
hasText
(
request
.
getPassword
())
?
request
.
getPassword
()
:
"YaoAI-"
+
RandomUtil
.
randomString
(
10
);
AdminUser
user
=
new
AdminUser
();
user
.
setUsername
(
request
.
getUsername
());
user
.
setPasswordHash
(
BCrypt
.
hashpw
(
password
,
BCrypt
.
gensalt
()));
user
.
setDisplayName
(
request
.
getDisplayName
());
user
.
setEmail
(
request
.
getEmail
());
user
.
setRole
(
request
.
getRole
());
user
.
setStatus
(
1
);
user
.
setMustChangePassword
(
1
);
user
.
setCreatedBy
(
StpAdminUtil
.
getLoginIdAsLong
());
user
.
setCreatedAt
(
LocalDateTime
.
now
());
user
.
setUpdatedAt
(
LocalDateTime
.
now
());
adminUserMapper
.
insert
(
user
);
return
adminUserMapper
.
selectById
(
user
.
getId
());
}
@Transactional
public
AdminUser
update
(
Long
id
,
AdminUserUpdateRequest
request
)
{
AdminUser
user
=
requireAdminUser
(
id
);
if
(
request
.
getDisplayName
()
!=
null
)
user
.
setDisplayName
(
request
.
getDisplayName
());
if
(
request
.
getEmail
()
!=
null
)
user
.
setEmail
(
request
.
getEmail
());
if
(
request
.
getRole
()
!=
null
)
{
if
(!
ROLES
.
contains
(
request
.
getRole
()))
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"运营角色无效"
);
}
user
.
setRole
(
request
.
getRole
());
}
if
(
request
.
getStatus
()
!=
null
)
{
if
(
request
.
getStatus
()
!=
0
&&
request
.
getStatus
()
!=
1
)
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"账号状态必须为 0 或 1"
);
}
user
.
setStatus
(
request
.
getStatus
());
}
user
.
setUpdatedAt
(
LocalDateTime
.
now
());
adminUserMapper
.
updateById
(
user
);
return
adminUserMapper
.
selectById
(
id
);
}
@Transactional
public
ResetPasswordResponse
resetPassword
(
Long
id
)
{
AdminUser
user
=
requireAdminUser
(
id
);
String
password
=
"YaoAI-"
+
RandomUtil
.
randomString
(
10
);
user
.
setPasswordHash
(
BCrypt
.
hashpw
(
password
,
BCrypt
.
gensalt
()));
user
.
setMustChangePassword
(
1
);
user
.
setUpdatedAt
(
LocalDateTime
.
now
());
adminUserMapper
.
updateById
(
user
);
StpAdminUtil
.
INSTANCE
.
kickout
(
id
);
return
new
ResetPasswordResponse
(
password
);
}
private
LambdaQueryWrapper
<
AdminUser
>
buildQuery
(
AdminUserQuery
query
)
{
LambdaQueryWrapper
<
AdminUser
>
wrapper
=
Wrappers
.<
AdminUser
>
lambdaQuery
();
if
(
StringUtils
.
hasText
(
query
.
getKeyword
()))
{
wrapper
.
and
(
w
->
w
.
like
(
AdminUser:
:
getUsername
,
query
.
getKeyword
())
.
or
()
.
like
(
AdminUser:
:
getDisplayName
,
query
.
getKeyword
())
.
or
()
.
like
(
AdminUser:
:
getEmail
,
query
.
getKeyword
()));
}
if
(
StringUtils
.
hasText
(
query
.
getRole
()))
{
wrapper
.
eq
(
AdminUser:
:
getRole
,
query
.
getRole
());
}
if
(
query
.
getStatus
()
!=
null
)
{
wrapper
.
eq
(
AdminUser:
:
getStatus
,
query
.
getStatus
());
}
return
wrapper
;
}
private
AdminUser
requireAdminUser
(
Long
id
)
{
AdminUser
user
=
adminUserMapper
.
selectById
(
id
);
if
(
user
==
null
)
{
throw
new
BizException
(
ErrorCode
.
NOT_FOUND
,
"运营账号不存在"
);
}
return
user
;
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/adminuser/dto/AdminUserCreateRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
adminuser
.
dto
;
import
lombok.Data
;
@Data
public
class
AdminUserCreateRequest
{
private
String
username
;
private
String
password
;
private
String
displayName
;
private
String
email
;
private
String
role
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/adminuser/dto/AdminUserQuery.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
adminuser
.
dto
;
import
lombok.Data
;
@Data
public
class
AdminUserQuery
{
private
String
keyword
;
private
String
role
;
private
Integer
status
;
private
long
page
=
1
;
private
long
size
=
20
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/adminuser/dto/AdminUserUpdateRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
adminuser
.
dto
;
import
lombok.Data
;
@Data
public
class
AdminUserUpdateRequest
{
private
String
displayName
;
private
String
email
;
private
String
role
;
private
Integer
status
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/audit/AdminAuditLogController.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
audit
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
com.yaoai.admin.audit.dto.AuditLogQuery
;
import
com.yaoai.admin.common.PageResult
;
import
com.yaoai.common.result.Result
;
import
com.yaoai.domain.entity.AdminAuditLog
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.ModelAttribute
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
@RestController
@RequestMapping
(
"/admin-api/audit-logs"
)
@RequiredArgsConstructor
@SaCheckLogin
(
type
=
"admin"
)
public
class
AdminAuditLogController
{
private
final
AdminAuditQueryService
adminAuditQueryService
;
@GetMapping
public
Result
<
PageResult
<
AdminAuditLog
>>
list
(
@ModelAttribute
AuditLogQuery
query
)
{
return
Result
.
ok
(
adminAuditQueryService
.
list
(
query
));
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/audit/AdminAuditQueryService.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
audit
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.core.toolkit.Wrappers
;
import
com.yaoai.admin.audit.dto.AuditLogQuery
;
import
com.yaoai.admin.auth.StpAdminUtil
;
import
com.yaoai.admin.common.PageResult
;
import
com.yaoai.domain.entity.AdminAuditLog
;
import
com.yaoai.domain.mapper.AdminAuditLogMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.StringUtils
;
@Service
@RequiredArgsConstructor
public
class
AdminAuditQueryService
{
private
final
AdminAuditLogMapper
adminAuditLogMapper
;
public
PageResult
<
AdminAuditLog
>
list
(
AuditLogQuery
query
)
{
long
page
=
Math
.
max
(
1
,
query
.
getPage
());
long
size
=
Math
.
max
(
1
,
Math
.
min
(
query
.
getSize
(),
100
));
long
offset
=
(
page
-
1
)
*
size
;
LambdaQueryWrapper
<
AdminAuditLog
>
wrapper
=
buildQuery
(
query
);
long
total
=
adminAuditLogMapper
.
selectCount
(
wrapper
);
return
new
PageResult
<>(
adminAuditLogMapper
.
selectList
(
buildQuery
(
query
)
.
orderByDesc
(
AdminAuditLog:
:
getCreatedAt
)
.
last
(
"LIMIT "
+
size
+
" OFFSET "
+
offset
)),
total
,
page
,
size
);
}
private
LambdaQueryWrapper
<
AdminAuditLog
>
buildQuery
(
AuditLogQuery
query
)
{
LambdaQueryWrapper
<
AdminAuditLog
>
wrapper
=
Wrappers
.<
AdminAuditLog
>
lambdaQuery
();
boolean
superAdmin
=
"SUPER_ADMIN"
.
equals
(
StpAdminUtil
.
getRole
());
if
(
superAdmin
&&
query
.
getAdminId
()
!=
null
)
{
wrapper
.
eq
(
AdminAuditLog:
:
getAdminUserId
,
query
.
getAdminId
());
}
else
if
(!
superAdmin
)
{
wrapper
.
eq
(
AdminAuditLog:
:
getAdminUserId
,
StpAdminUtil
.
getLoginIdAsLong
());
}
if
(
StringUtils
.
hasText
(
query
.
getAction
()))
{
wrapper
.
eq
(
AdminAuditLog:
:
getAction
,
query
.
getAction
());
}
if
(
StringUtils
.
hasText
(
query
.
getResourceType
()))
{
wrapper
.
eq
(
AdminAuditLog:
:
getResourceType
,
query
.
getResourceType
());
}
if
(
query
.
getFrom
()
!=
null
)
{
wrapper
.
ge
(
AdminAuditLog:
:
getCreatedAt
,
query
.
getFrom
());
}
if
(
query
.
getTo
()
!=
null
)
{
wrapper
.
le
(
AdminAuditLog:
:
getCreatedAt
,
query
.
getTo
());
}
return
wrapper
;
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/audit/AdminAuditService.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
audit
;
import
com.yaoai.domain.entity.AdminAuditLog
;
import
com.yaoai.domain.mapper.AdminAuditLogMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.stereotype.Service
;
@Service
@RequiredArgsConstructor
public
class
AdminAuditService
{
private
final
AdminAuditLogMapper
adminAuditLogMapper
;
public
void
record
(
AdminAuditLog
log
)
{
adminAuditLogMapper
.
insert
(
log
);
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/audit/annotation/AdminAudited.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
audit
.
annotation
;
import
java.lang.annotation.ElementType
;
import
java.lang.annotation.Retention
;
import
java.lang.annotation.RetentionPolicy
;
import
java.lang.annotation.Target
;
@Target
(
ElementType
.
METHOD
)
@Retention
(
RetentionPolicy
.
RUNTIME
)
public
@interface
AdminAudited
{
String
action
()
default
""
;
String
resource
()
default
""
;
String
resourceIdParam
()
default
""
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/audit/aspect/AdminAuditAspect.java
0 → 100644
View file @
b7185a90
This diff is collapsed.
Click to expand it.
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/audit/dto/AuditLogQuery.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
audit
.
dto
;
import
lombok.Data
;
import
org.springframework.format.annotation.DateTimeFormat
;
import
java.time.LocalDateTime
;
@Data
public
class
AuditLogQuery
{
private
Long
adminId
;
private
String
action
;
private
String
resourceType
;
@DateTimeFormat
(
iso
=
DateTimeFormat
.
ISO
.
DATE_TIME
)
private
LocalDateTime
from
;
@DateTimeFormat
(
iso
=
DateTimeFormat
.
ISO
.
DATE_TIME
)
private
LocalDateTime
to
;
private
long
page
=
1
;
private
long
size
=
20
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/auth/AdminAuthController.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
auth
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
com.yaoai.common.result.Result
;
import
com.yaoai.domain.entity.AdminUser
;
import
jakarta.servlet.http.HttpServletRequest
;
import
lombok.Data
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PostMapping
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
@RestController
@RequestMapping
(
"/admin-api/auth"
)
@RequiredArgsConstructor
public
class
AdminAuthController
{
private
final
AdminAuthService
authService
;
@PostMapping
(
"/login"
)
public
Result
<
String
>
login
(
@RequestBody
LoginRequest
req
,
HttpServletRequest
request
)
{
String
ip
=
getClientIp
(
request
);
String
token
=
authService
.
login
(
req
.
getUsername
(),
req
.
getPassword
(),
ip
);
return
Result
.
ok
(
token
);
}
@PostMapping
(
"/logout"
)
@SaCheckLogin
(
type
=
"admin"
)
public
Result
<
Void
>
logout
()
{
authService
.
logout
();
return
Result
.
ok
();
}
@GetMapping
(
"/me"
)
@SaCheckLogin
(
type
=
"admin"
)
public
Result
<
AdminUser
>
me
()
{
return
Result
.
ok
(
authService
.
getCurrentAdmin
());
}
@PostMapping
(
"/change-password"
)
@SaCheckLogin
(
type
=
"admin"
)
public
Result
<
Void
>
changePassword
(
@RequestBody
ChangePasswordRequest
req
)
{
authService
.
changePassword
(
req
.
getOldPassword
(),
req
.
getNewPassword
());
return
Result
.
ok
();
}
private
String
getClientIp
(
HttpServletRequest
request
)
{
String
ip
=
request
.
getHeader
(
"X-Forwarded-For"
);
if
(
ip
==
null
||
ip
.
isEmpty
()
||
"unknown"
.
equalsIgnoreCase
(
ip
))
{
ip
=
request
.
getHeader
(
"X-Real-IP"
);
}
if
(
ip
==
null
||
ip
.
isEmpty
()
||
"unknown"
.
equalsIgnoreCase
(
ip
))
{
ip
=
request
.
getRemoteAddr
();
}
return
ip
;
}
@Data
static
class
LoginRequest
{
private
String
username
;
private
String
password
;
}
@Data
static
class
ChangePasswordRequest
{
private
String
oldPassword
;
private
String
newPassword
;
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/auth/AdminAuthService.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
auth
;
import
cn.hutool.crypto.digest.BCrypt
;
import
com.baomidou.mybatisplus.core.toolkit.Wrappers
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.AdminUser
;
import
com.yaoai.domain.mapper.AdminUserMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.StringUtils
;
import
java.time.LocalDateTime
;
@Service
@RequiredArgsConstructor
public
class
AdminAuthService
{
private
final
AdminUserMapper
adminUserMapper
;
public
String
login
(
String
username
,
String
password
,
String
ip
)
{
AdminUser
user
=
adminUserMapper
.
selectOne
(
Wrappers
.<
AdminUser
>
lambdaQuery
()
.
eq
(
AdminUser:
:
getUsername
,
username
));
if
(
user
==
null
)
{
throw
new
BizException
(
ErrorCode
.
UNAUTHORIZED
.
getCode
(),
"用户名或密码错误"
);
}
if
(
Integer
.
valueOf
(
0
).
equals
(
user
.
getStatus
()))
{
throw
new
BizException
(
ErrorCode
.
FORBIDDEN
.
getCode
(),
"账号已被禁用"
);
}
if
(!
BCrypt
.
checkpw
(
password
,
user
.
getPasswordHash
()))
{
throw
new
BizException
(
ErrorCode
.
UNAUTHORIZED
.
getCode
(),
"用户名或密码错误"
);
}
StpAdminUtil
.
login
(
user
.
getId
());
user
.
setLastLoginAt
(
LocalDateTime
.
now
());
user
.
setLastLoginIp
(
ip
);
adminUserMapper
.
updateById
(
user
);
return
StpAdminUtil
.
getTokenValue
();
}
public
void
logout
()
{
StpAdminUtil
.
logout
();
}
public
AdminUser
getCurrentAdmin
()
{
Long
id
=
StpAdminUtil
.
getLoginIdAsLong
();
return
adminUserMapper
.
selectById
(
id
);
}
public
void
changePassword
(
String
oldPassword
,
String
newPassword
)
{
if
(!
StringUtils
.
hasText
(
oldPassword
)
||
!
StringUtils
.
hasText
(
newPassword
))
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
.
getCode
(),
"旧密码和新密码不能为空"
);
}
if
(
newPassword
.
length
()
<
8
)
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
.
getCode
(),
"新密码长度不能少于 8 位"
);
}
Long
id
=
StpAdminUtil
.
getLoginIdAsLong
();
AdminUser
user
=
adminUserMapper
.
selectById
(
id
);
if
(
user
==
null
)
{
throw
new
BizException
(
ErrorCode
.
UNAUTHORIZED
.
getCode
(),
"登录已失效"
);
}
if
(!
BCrypt
.
checkpw
(
oldPassword
,
user
.
getPasswordHash
()))
{
throw
new
BizException
(
ErrorCode
.
UNAUTHORIZED
.
getCode
(),
"旧密码错误"
);
}
user
.
setPasswordHash
(
BCrypt
.
hashpw
(
newPassword
,
BCrypt
.
gensalt
()));
user
.
setMustChangePassword
(
0
);
user
.
setUpdatedAt
(
LocalDateTime
.
now
());
adminUserMapper
.
updateById
(
user
);
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/auth/AdminPermissionInterface.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
auth
;
import
com.yaoai.domain.entity.AdminUser
;
import
com.yaoai.domain.mapper.AdminUserMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.stereotype.Component
;
import
java.util.Collections
;
import
java.util.List
;
@Component
@RequiredArgsConstructor
public
class
AdminPermissionInterface
implements
cn
.
dev33
.
satoken
.
stp
.
StpInterface
{
private
final
AdminUserMapper
adminUserMapper
;
@Override
public
List
<
String
>
getPermissionList
(
Object
loginId
,
String
loginType
)
{
return
Collections
.
emptyList
();
}
@Override
public
List
<
String
>
getRoleList
(
Object
loginId
,
String
loginType
)
{
if
(!
"admin"
.
equals
(
loginType
))
{
return
Collections
.
emptyList
();
}
AdminUser
user
=
adminUserMapper
.
selectById
(
Long
.
parseLong
(
loginId
.
toString
()));
if
(
user
==
null
||
user
.
getRole
()
==
null
)
{
return
Collections
.
emptyList
();
}
return
List
.
of
(
user
.
getRole
());
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/auth/StpAdminUtil.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
auth
;
import
cn.dev33.satoken.SaManager
;
import
cn.dev33.satoken.config.SaTokenConfig
;
import
cn.dev33.satoken.session.SaSession
;
import
cn.dev33.satoken.stp.StpLogic
;
import
org.springframework.stereotype.Component
;
import
java.util.List
;
@Component
public
class
StpAdminUtil
{
public
static
final
StpLogic
INSTANCE
=
new
StpLogic
(
"admin"
);
static
{
INSTANCE
.
setConfig
(
new
SaTokenConfig
().
setTokenName
(
"satoken-admin"
));
SaManager
.
putStpLogic
(
INSTANCE
);
}
public
static
void
login
(
Long
adminId
)
{
INSTANCE
.
login
(
adminId
);
}
public
static
long
getLoginIdAsLong
()
{
return
INSTANCE
.
getLoginIdAsLong
();
}
public
static
String
getTokenValue
()
{
return
INSTANCE
.
getTokenValue
();
}
public
static
void
logout
()
{
INSTANCE
.
logout
();
}
public
static
boolean
isLogin
()
{
return
INSTANCE
.
isLogin
();
}
public
static
SaSession
getSession
()
{
return
INSTANCE
.
getSession
();
}
public
static
String
getRole
()
{
List
<
String
>
roles
=
INSTANCE
.
getRoleList
();
return
roles
.
isEmpty
()
?
"default-role"
:
roles
.
get
(
0
);
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/billing/BillingAdminController.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
billing
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
cn.dev33.satoken.annotation.SaCheckRole
;
import
com.yaoai.admin.audit.annotation.AdminAudited
;
import
com.yaoai.admin.auth.StpAdminUtil
;
import
com.yaoai.admin.billing.dto.BillingCostItem
;
import
com.yaoai.admin.billing.dto.BillingCostUpdateRequest
;
import
com.yaoai.admin.billing.dto.BillingRecordQuery
;
import
com.yaoai.admin.billing.dto.BillingSummaryItem
;
import
com.yaoai.admin.billing.dto.UsageDetailResponse
;
import
com.yaoai.admin.common.PageResult
;
import
com.yaoai.common.result.Result
;
import
com.yaoai.domain.entity.BillingRecord
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.format.annotation.DateTimeFormat
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.ModelAttribute
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.PutMapping
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RestController
;
import
java.time.LocalDateTime
;
import
java.util.List
;
@RestController
@RequiredArgsConstructor
@SaCheckLogin
(
type
=
"admin"
)
public
class
BillingAdminController
{
private
final
BillingAdminService
billingAdminService
;
@GetMapping
(
"/admin-api/billing/records"
)
public
Result
<
PageResult
<
BillingRecord
>>
records
(
@ModelAttribute
BillingRecordQuery
query
)
{
return
Result
.
ok
(
billingAdminService
.
records
(
query
));
}
@GetMapping
(
"/admin-api/billing/recharges"
)
public
Result
<
PageResult
<
BillingRecord
>>
recharges
(
@ModelAttribute
BillingRecordQuery
query
)
{
return
Result
.
ok
(
billingAdminService
.
recharges
(
query
));
}
@GetMapping
(
"/admin-api/billing/consumes"
)
public
Result
<
PageResult
<
BillingRecord
>>
consumes
(
@ModelAttribute
BillingRecordQuery
query
)
{
return
Result
.
ok
(
billingAdminService
.
consumes
(
query
));
}
@GetMapping
(
"/admin-api/billing/summary"
)
public
Result
<
List
<
BillingSummaryItem
>>
summary
(
@RequestParam
(
defaultValue
=
"modality"
)
String
dim
,
@ModelAttribute
BillingRecordQuery
query
)
{
return
Result
.
ok
(
billingAdminService
.
summary
(
dim
,
query
));
}
@GetMapping
(
"/admin-api/billing/costs"
)
@SaCheckRole
(
value
=
"SUPER_ADMIN"
,
type
=
"admin"
)
public
Result
<
List
<
BillingCostItem
>>
costs
()
{
return
Result
.
ok
(
billingAdminService
.
costs
());
}
@PutMapping
(
"/admin-api/billing/costs"
)
@AdminAudited
(
action
=
"BILLING_COST_UPDATE"
,
resource
=
"BILLING_COST"
)
@SaCheckRole
(
value
=
"SUPER_ADMIN"
,
type
=
"admin"
)
public
Result
<
List
<
BillingCostItem
>>
saveCosts
(
@RequestBody
BillingCostUpdateRequest
request
)
{
return
Result
.
ok
(
billingAdminService
.
saveCosts
(
request
.
getCosts
(),
StpAdminUtil
.
getLoginIdAsLong
()));
}
@GetMapping
(
"/admin-api/tenants/{id}/usage-detail"
)
public
Result
<
UsageDetailResponse
>
usageDetail
(
@PathVariable
Long
id
,
@RequestParam
(
required
=
false
)
@DateTimeFormat
(
iso
=
DateTimeFormat
.
ISO
.
DATE_TIME
)
LocalDateTime
from
,
@RequestParam
(
required
=
false
)
@DateTimeFormat
(
iso
=
DateTimeFormat
.
ISO
.
DATE_TIME
)
LocalDateTime
to
,
@RequestParam
(
defaultValue
=
"model"
)
String
groupBy
)
{
return
Result
.
ok
(
billingAdminService
.
usageDetail
(
id
,
from
,
to
,
groupBy
));
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/billing/BillingAdminService.java
0 → 100644
View file @
b7185a90
This diff is collapsed.
Click to expand it.
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/billing/dto/BillingCostItem.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
billing
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.math.BigDecimal
;
@Data
@Builder
public
class
BillingCostItem
{
private
String
operation
;
private
String
label
;
private
BigDecimal
cost
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/billing/dto/BillingCostUpdateRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
billing
.
dto
;
import
lombok.Data
;
import
java.math.BigDecimal
;
import
java.util.Map
;
@Data
public
class
BillingCostUpdateRequest
{
private
Map
<
String
,
BigDecimal
>
costs
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/billing/dto/BillingRecordQuery.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
billing
.
dto
;
import
lombok.Data
;
import
org.springframework.format.annotation.DateTimeFormat
;
import
java.time.LocalDateTime
;
@Data
public
class
BillingRecordQuery
{
private
String
modality
;
private
String
modelId
;
private
Long
tenantId
;
private
Long
userId
;
@DateTimeFormat
(
iso
=
DateTimeFormat
.
ISO
.
DATE_TIME
)
private
LocalDateTime
from
;
@DateTimeFormat
(
iso
=
DateTimeFormat
.
ISO
.
DATE_TIME
)
private
LocalDateTime
to
;
private
long
page
=
1
;
private
long
size
=
20
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/billing/dto/BillingSummaryItem.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
billing
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.math.BigDecimal
;
@Data
@Builder
public
class
BillingSummaryItem
{
private
String
key
;
private
String
label
;
private
BigDecimal
totalCredits
;
private
long
totalUnits
;
private
long
count
;
private
long
successCount
;
private
long
failCount
;
private
BigDecimal
percent
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/billing/dto/UsageDetailResponse.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
billing
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.math.BigDecimal
;
import
java.util.List
;
@Data
@Builder
public
class
UsageDetailResponse
{
private
Long
tenantId
;
private
String
groupBy
;
private
BigDecimal
total
;
private
List
<
BillingSummaryItem
>
breakdown
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/common/PageResult.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
common
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
java.util.List
;
@Data
@AllArgsConstructor
public
class
PageResult
<
T
>
{
private
List
<
T
>
records
;
private
long
total
;
private
long
page
;
private
long
size
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/config/AdminGlobalExceptionHandler.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
config
;
import
cn.dev33.satoken.exception.NotLoginException
;
import
cn.dev33.satoken.exception.NotPermissionException
;
import
cn.dev33.satoken.exception.NotRoleException
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.common.result.Result
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.core.Ordered
;
import
org.springframework.core.annotation.Order
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.validation.FieldError
;
import
org.springframework.web.bind.MethodArgumentNotValidException
;
import
org.springframework.web.bind.annotation.ExceptionHandler
;
import
org.springframework.web.bind.annotation.ResponseStatus
;
import
org.springframework.web.bind.annotation.RestControllerAdvice
;
import
java.util.stream.Collectors
;
@Slf4j
@Order
(
Ordered
.
HIGHEST_PRECEDENCE
)
@RestControllerAdvice
(
basePackages
=
"com.yaoai.admin"
)
public
class
AdminGlobalExceptionHandler
{
@ExceptionHandler
(
NotLoginException
.
class
)
@ResponseStatus
(
HttpStatus
.
UNAUTHORIZED
)
public
Result
<?>
handleNotLogin
(
NotLoginException
e
)
{
return
Result
.
error
(
ErrorCode
.
UNAUTHORIZED
.
getCode
(),
"请先登录运营后台"
);
}
@ExceptionHandler
({
NotPermissionException
.
class
,
NotRoleException
.
class
})
@ResponseStatus
(
HttpStatus
.
FORBIDDEN
)
public
Result
<?>
handleForbidden
(
RuntimeException
e
)
{
return
Result
.
error
(
ErrorCode
.
FORBIDDEN
.
getCode
(),
"无权访问运营后台"
);
}
@ExceptionHandler
(
BizException
.
class
)
@ResponseStatus
(
HttpStatus
.
OK
)
public
Result
<?>
handleBizException
(
BizException
e
)
{
log
.
warn
(
"Admin BizException [{}]: {}"
,
e
.
getCode
(),
e
.
getMessage
());
return
Result
.
error
(
e
.
getCode
(),
e
.
getMessage
());
}
@ExceptionHandler
(
MethodArgumentNotValidException
.
class
)
@ResponseStatus
(
HttpStatus
.
BAD_REQUEST
)
public
Result
<?>
handleValidationException
(
MethodArgumentNotValidException
e
)
{
String
message
=
e
.
getBindingResult
().
getFieldErrors
().
stream
()
.
map
(
FieldError:
:
getDefaultMessage
)
.
collect
(
Collectors
.
joining
(
"; "
));
return
Result
.
error
(
ErrorCode
.
INVALID_PARAM
.
getCode
(),
message
);
}
@ExceptionHandler
(
IllegalArgumentException
.
class
)
@ResponseStatus
(
HttpStatus
.
BAD_REQUEST
)
public
Result
<?>
handleIllegalArgument
(
IllegalArgumentException
e
)
{
return
Result
.
error
(
ErrorCode
.
INVALID_PARAM
.
getCode
(),
e
.
getMessage
());
}
@ExceptionHandler
(
Exception
.
class
)
@ResponseStatus
(
HttpStatus
.
INTERNAL_SERVER_ERROR
)
public
Result
<?>
handleException
(
Exception
e
)
{
log
.
error
(
"Unexpected admin error"
,
e
);
return
Result
.
error
(
ErrorCode
.
INTERNAL_ERROR
.
getCode
(),
"服务器内部错误"
);
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/config/SaTokenAdminConfig.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
config
;
import
cn.dev33.satoken.interceptor.SaInterceptor
;
import
com.yaoai.admin.auth.StpAdminUtil
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.web.servlet.config.annotation.InterceptorRegistry
;
import
org.springframework.web.servlet.config.annotation.WebMvcConfigurer
;
@Configuration
public
class
SaTokenAdminConfig
implements
WebMvcConfigurer
{
@Override
public
void
addInterceptors
(
InterceptorRegistry
registry
)
{
registry
.
addInterceptor
(
new
SaInterceptor
(
handle
->
StpAdminUtil
.
INSTANCE
.
checkLogin
()))
.
addPathPatterns
(
"/admin-api/**"
)
.
excludePathPatterns
(
"/admin-api/auth/login"
);
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/dashboard/DashboardController.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
dashboard
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
com.yaoai.admin.dashboard.dto.DashboardOverview
;
import
com.yaoai.admin.dashboard.dto.DashboardProviderStats
;
import
com.yaoai.admin.dashboard.dto.DashboardTaskStats
;
import
com.yaoai.admin.dashboard.dto.DashboardTrendPoint
;
import
com.yaoai.common.result.Result
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
org.springframework.web.bind.annotation.RestController
;
import
java.util.List
;
@RestController
@RequestMapping
(
"/admin-api/dashboard"
)
@RequiredArgsConstructor
@SaCheckLogin
(
type
=
"admin"
)
public
class
DashboardController
{
private
final
DashboardService
dashboardService
;
@GetMapping
(
"/overview"
)
public
Result
<
DashboardOverview
>
overview
()
{
return
Result
.
ok
(
dashboardService
.
overview
());
}
@GetMapping
(
"/trends"
)
public
Result
<
List
<
DashboardTrendPoint
>>
trends
(
@RequestParam
(
defaultValue
=
"30"
)
int
days
)
{
return
Result
.
ok
(
dashboardService
.
trends
(
days
));
}
@GetMapping
(
"/tasks"
)
public
Result
<
List
<
DashboardTaskStats
>>
tasks
()
{
return
Result
.
ok
(
dashboardService
.
tasks
());
}
@GetMapping
(
"/providers"
)
public
Result
<
List
<
DashboardProviderStats
>>
providers
()
{
return
Result
.
ok
(
dashboardService
.
providers
());
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/dashboard/DashboardService.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
dashboard
;
import
com.baomidou.mybatisplus.core.toolkit.Wrappers
;
import
com.yaoai.admin.dashboard.dto.DashboardOverview
;
import
com.yaoai.admin.dashboard.dto.DashboardProviderStats
;
import
com.yaoai.admin.dashboard.dto.DashboardTaskStats
;
import
com.yaoai.admin.dashboard.dto.DashboardTrendPoint
;
import
com.yaoai.domain.entity.AiTask
;
import
com.yaoai.domain.entity.BillingRecord
;
import
com.yaoai.domain.entity.Tenant
;
import
com.yaoai.domain.entity.User
;
import
com.yaoai.domain.mapper.AiTaskMapper
;
import
com.yaoai.domain.mapper.BillingRecordMapper
;
import
com.yaoai.domain.mapper.TenantMapper
;
import
com.yaoai.domain.mapper.UserMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.stereotype.Service
;
import
java.math.BigDecimal
;
import
java.time.LocalDate
;
import
java.time.LocalDateTime
;
import
java.util.ArrayList
;
import
java.util.Comparator
;
import
java.util.LinkedHashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Objects
;
import
java.util.stream.Collectors
;
@Service
@RequiredArgsConstructor
public
class
DashboardService
{
private
final
TenantMapper
tenantMapper
;
private
final
UserMapper
userMapper
;
private
final
BillingRecordMapper
billingRecordMapper
;
private
final
AiTaskMapper
aiTaskMapper
;
public
DashboardOverview
overview
()
{
LocalDateTime
todayStart
=
LocalDate
.
now
().
atStartOfDay
();
List
<
BillingRecord
>
todayRecords
=
billingRecordMapper
.
selectList
(
Wrappers
.<
BillingRecord
>
lambdaQuery
()
.
ge
(
BillingRecord:
:
getCreatedAt
,
todayStart
));
BigDecimal
consumed
=
todayRecords
.
stream
()
.
filter
(
this
::
isConsume
)
.
map
(
BillingRecord:
:
getCredits
)
.
filter
(
Objects:
:
nonNull
)
.
reduce
(
BigDecimal
.
ZERO
,
BigDecimal:
:
add
);
BigDecimal
recharged
=
todayRecords
.
stream
()
.
filter
(
this
::
isRecharge
)
.
map
(
BillingRecord:
:
getCredits
)
.
filter
(
Objects:
:
nonNull
)
.
reduce
(
BigDecimal
.
ZERO
,
BigDecimal:
:
add
);
return
DashboardOverview
.
builder
()
.
totalTenants
(
countActiveRows
())
.
activeTenants
(
tenantMapper
.
selectCount
(
Wrappers
.<
Tenant
>
lambdaQuery
()
.
eq
(
Tenant:
:
getStatus
,
1
)
.
isNull
(
Tenant:
:
getDeletedAt
)))
.
totalUsers
(
userMapper
.
selectCount
(
Wrappers
.<
User
>
lambdaQuery
().
isNull
(
User:
:
getDeletedAt
)))
.
todayNewTenants
(
tenantMapper
.
selectCount
(
Wrappers
.<
Tenant
>
lambdaQuery
()
.
ge
(
Tenant:
:
getCreatedAt
,
todayStart
)
.
isNull
(
Tenant:
:
getDeletedAt
)))
.
todayConsumedCredits
(
consumed
)
.
todayRechargedCredits
(
recharged
)
.
build
();
}
public
List
<
DashboardTrendPoint
>
trends
(
int
days
)
{
int
safeDays
=
Math
.
max
(
1
,
Math
.
min
(
days
,
90
));
LocalDate
startDay
=
LocalDate
.
now
().
minusDays
(
safeDays
-
1L
);
LocalDateTime
start
=
startDay
.
atStartOfDay
();
List
<
Tenant
>
tenants
=
tenantMapper
.
selectList
(
Wrappers
.<
Tenant
>
lambdaQuery
()
.
ge
(
Tenant:
:
getCreatedAt
,
start
)
.
isNull
(
Tenant:
:
getDeletedAt
));
List
<
BillingRecord
>
records
=
billingRecordMapper
.
selectList
(
Wrappers
.<
BillingRecord
>
lambdaQuery
()
.
ge
(
BillingRecord:
:
getCreatedAt
,
start
));
Map
<
LocalDate
,
Long
>
tenantCounts
=
tenants
.
stream
()
.
filter
(
t
->
t
.
getCreatedAt
()
!=
null
)
.
collect
(
Collectors
.
groupingBy
(
t
->
t
.
getCreatedAt
().
toLocalDate
(),
Collectors
.
counting
()));
Map
<
LocalDate
,
BigDecimal
>
consumed
=
new
LinkedHashMap
<>();
Map
<
LocalDate
,
BigDecimal
>
recharged
=
new
LinkedHashMap
<>();
for
(
BillingRecord
record
:
records
)
{
if
(
record
.
getCreatedAt
()
==
null
||
record
.
getCredits
()
==
null
)
{
continue
;
}
LocalDate
day
=
record
.
getCreatedAt
().
toLocalDate
();
Map
<
LocalDate
,
BigDecimal
>
target
=
isRecharge
(
record
)
?
recharged
:
consumed
;
target
.
merge
(
day
,
record
.
getCredits
(),
BigDecimal:
:
add
);
}
List
<
DashboardTrendPoint
>
points
=
new
ArrayList
<>();
for
(
int
i
=
0
;
i
<
safeDays
;
i
++)
{
LocalDate
day
=
startDay
.
plusDays
(
i
);
points
.
add
(
DashboardTrendPoint
.
builder
()
.
day
(
day
)
.
newTenants
(
tenantCounts
.
getOrDefault
(
day
,
0L
))
.
consumedCredits
(
consumed
.
getOrDefault
(
day
,
BigDecimal
.
ZERO
))
.
rechargedCredits
(
recharged
.
getOrDefault
(
day
,
BigDecimal
.
ZERO
))
.
build
());
}
return
points
;
}
public
List
<
DashboardTaskStats
>
tasks
()
{
return
aiTaskMapper
.
selectList
(
Wrappers
.
emptyWrapper
()).
stream
()
.
collect
(
Collectors
.
groupingBy
(
task
->
blankToDefault
(
task
.
getTaskType
(),
"unknown"
)))
.
entrySet
().
stream
()
.
map
(
entry
->
toTaskStats
(
entry
.
getKey
(),
entry
.
getValue
()))
.
sorted
(
Comparator
.
comparing
(
DashboardTaskStats:
:
getTotal
).
reversed
())
.
toList
();
}
public
List
<
DashboardProviderStats
>
providers
()
{
return
billingRecordMapper
.
selectList
(
Wrappers
.<
BillingRecord
>
lambdaQuery
()
.
isNotNull
(
BillingRecord:
:
getModelProvider
))
.
stream
()
.
filter
(
this
::
isConsume
)
.
collect
(
Collectors
.
groupingBy
(
this
::
providerKey
))
.
entrySet
().
stream
()
.
map
(
entry
->
toProviderStats
(
entry
.
getValue
()))
.
sorted
(
Comparator
.
comparing
(
DashboardProviderStats:
:
getCredits
).
reversed
())
.
toList
();
}
private
long
countActiveRows
()
{
return
tenantMapper
.
selectCount
(
Wrappers
.<
Tenant
>
lambdaQuery
().
isNull
(
Tenant:
:
getDeletedAt
));
}
private
DashboardTaskStats
toTaskStats
(
String
taskType
,
List
<
AiTask
>
tasks
)
{
long
total
=
tasks
.
size
();
long
succeeded
=
tasks
.
stream
().
filter
(
t
->
"succeeded"
.
equals
(
t
.
getStatus
())).
count
();
long
failed
=
tasks
.
stream
().
filter
(
t
->
"failed"
.
equals
(
t
.
getStatus
())).
count
();
return
DashboardTaskStats
.
builder
()
.
taskType
(
taskType
)
.
total
(
total
)
.
succeeded
(
succeeded
)
.
failed
(
failed
)
.
successRate
(
total
==
0
?
0
:
succeeded
*
1.0
/
total
)
.
build
();
}
private
DashboardProviderStats
toProviderStats
(
List
<
BillingRecord
>
records
)
{
BillingRecord
first
=
records
.
get
(
0
);
long
total
=
records
.
size
();
long
succeeded
=
records
.
stream
().
filter
(
r
->
!
Boolean
.
FALSE
.
equals
(
r
.
getSuccess
())).
count
();
long
failed
=
total
-
succeeded
;
BigDecimal
credits
=
records
.
stream
()
.
map
(
BillingRecord:
:
getCredits
)
.
filter
(
Objects:
:
nonNull
)
.
reduce
(
BigDecimal
.
ZERO
,
BigDecimal:
:
add
);
return
DashboardProviderStats
.
builder
()
.
modelProvider
(
first
.
getModelProvider
())
.
modelId
(
first
.
getModelId
())
.
modelName
(
first
.
getModelName
())
.
total
(
total
)
.
succeeded
(
succeeded
)
.
failed
(
failed
)
.
credits
(
credits
)
.
failureRate
(
total
==
0
?
0
:
failed
*
1.0
/
total
)
.
build
();
}
private
String
providerKey
(
BillingRecord
record
)
{
return
blankToDefault
(
record
.
getModelProvider
(),
"unknown"
)
+
"|"
+
blankToDefault
(
record
.
getModelId
(),
"unknown"
)
+
"|"
+
blankToDefault
(
record
.
getModelName
(),
"unknown"
);
}
private
boolean
isConsume
(
BillingRecord
record
)
{
return
!
isRecharge
(
record
);
}
private
boolean
isRecharge
(
BillingRecord
record
)
{
return
"RECHARGE"
.
equals
(
record
.
getModality
())
||
"recharge"
.
equals
(
record
.
getOperation
());
}
private
String
blankToDefault
(
String
value
,
String
defaultValue
)
{
return
value
==
null
||
value
.
isBlank
()
?
defaultValue
:
value
;
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/dashboard/dto/DashboardOverview.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
dashboard
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.math.BigDecimal
;
@Data
@Builder
public
class
DashboardOverview
{
private
long
totalTenants
;
private
long
activeTenants
;
private
long
totalUsers
;
private
long
todayNewTenants
;
private
BigDecimal
todayConsumedCredits
;
private
BigDecimal
todayRechargedCredits
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/dashboard/dto/DashboardProviderStats.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
dashboard
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.math.BigDecimal
;
@Data
@Builder
public
class
DashboardProviderStats
{
private
String
modelProvider
;
private
String
modelId
;
private
String
modelName
;
private
long
total
;
private
long
succeeded
;
private
long
failed
;
private
BigDecimal
credits
;
private
double
failureRate
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/dashboard/dto/DashboardTaskStats.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
dashboard
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
@Data
@Builder
public
class
DashboardTaskStats
{
private
String
taskType
;
private
long
total
;
private
long
succeeded
;
private
long
failed
;
private
double
successRate
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/dashboard/dto/DashboardTrendPoint.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
dashboard
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.math.BigDecimal
;
import
java.time.LocalDate
;
@Data
@Builder
public
class
DashboardTrendPoint
{
private
LocalDate
day
;
private
long
newTenants
;
private
BigDecimal
consumedCredits
;
private
BigDecimal
rechargedCredits
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/plan/PlanAdminController.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
plan
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
cn.dev33.satoken.annotation.SaCheckRole
;
import
com.yaoai.admin.audit.annotation.AdminAudited
;
import
com.yaoai.admin.plan.dto.PlanUpdateRequest
;
import
com.yaoai.common.result.Result
;
import
com.yaoai.domain.entity.Plan
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PatchMapping
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
import
java.util.List
;
@RestController
@RequestMapping
(
"/admin-api/plans"
)
@RequiredArgsConstructor
@SaCheckLogin
(
type
=
"admin"
)
public
class
PlanAdminController
{
private
final
PlanAdminService
planAdminService
;
@GetMapping
public
Result
<
List
<
Plan
>>
list
()
{
return
Result
.
ok
(
planAdminService
.
list
());
}
@PatchMapping
(
"/{id}"
)
@AdminAudited
(
action
=
"PLAN_UPDATE"
,
resource
=
"PLAN"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
})
public
Result
<
Plan
>
update
(
@PathVariable
Long
id
,
@RequestBody
PlanUpdateRequest
request
)
{
return
Result
.
ok
(
planAdminService
.
update
(
id
,
request
));
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/plan/PlanAdminService.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
plan
;
import
com.yaoai.admin.auth.StpAdminUtil
;
import
com.yaoai.admin.plan.dto.PlanUpdateRequest
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.Plan
;
import
com.yaoai.domain.mapper.PlanMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
java.time.LocalDateTime
;
import
java.util.List
;
@Service
@RequiredArgsConstructor
public
class
PlanAdminService
{
private
final
PlanMapper
planMapper
;
public
List
<
Plan
>
list
()
{
return
planMapper
.
selectList
(
com
.
baomidou
.
mybatisplus
.
core
.
toolkit
.
Wrappers
.<
Plan
>
lambdaQuery
()
.
orderByAsc
(
Plan:
:
getId
));
}
@Transactional
public
Plan
update
(
Long
id
,
PlanUpdateRequest
request
)
{
Plan
plan
=
planMapper
.
selectById
(
id
);
if
(
plan
==
null
)
{
throw
new
BizException
(
ErrorCode
.
NOT_FOUND
,
"套餐不存在"
);
}
if
(
request
.
getName
()
!=
null
)
plan
.
setName
(
request
.
getName
());
if
(
request
.
getPriceMonthly
()
!=
null
)
plan
.
setPriceMonthly
(
request
.
getPriceMonthly
());
if
(
request
.
getMonthlyCredits
()
!=
null
)
plan
.
setMonthlyCredits
(
request
.
getMonthlyCredits
());
if
(
request
.
getMaxMembers
()
!=
null
)
plan
.
setMaxMembers
(
request
.
getMaxMembers
());
if
(
request
.
getMaxProjects
()
!=
null
)
plan
.
setMaxProjects
(
request
.
getMaxProjects
());
if
(
request
.
getMaxConcurrentTasks
()
!=
null
)
plan
.
setMaxConcurrentTasks
(
request
.
getMaxConcurrentTasks
());
if
(
request
.
getFeatures
()
!=
null
)
plan
.
setFeatures
(
request
.
getFeatures
());
if
(
request
.
getIsActive
()
!=
null
)
plan
.
setIsActive
(
request
.
getIsActive
());
plan
.
setUpdatedAt
(
LocalDateTime
.
now
());
plan
.
setUpdatedBy
(
StpAdminUtil
.
getLoginIdAsLong
());
planMapper
.
updateById
(
plan
);
return
planMapper
.
selectById
(
id
);
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/plan/dto/PlanUpdateRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
plan
.
dto
;
import
lombok.Data
;
import
java.math.BigDecimal
;
@Data
public
class
PlanUpdateRequest
{
private
String
name
;
private
BigDecimal
priceMonthly
;
private
Integer
monthlyCredits
;
private
Integer
maxMembers
;
private
Integer
maxProjects
;
private
Integer
maxConcurrentTasks
;
private
String
features
;
private
Integer
isActive
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/TenantAdminController.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
cn.dev33.satoken.annotation.SaCheckRole
;
import
cn.dev33.satoken.annotation.SaMode
;
import
com.yaoai.admin.audit.annotation.AdminAudited
;
import
com.yaoai.admin.common.PageResult
;
import
com.yaoai.admin.tenant.dto.TenantBalanceOverview
;
import
com.yaoai.admin.tenant.dto.TenantDetail
;
import
com.yaoai.admin.tenant.dto.TenantImpersonateRequest
;
import
com.yaoai.admin.tenant.dto.TenantImpersonateResponse
;
import
com.yaoai.admin.tenant.dto.TenantListItem
;
import
com.yaoai.admin.tenant.dto.TenantPlanChangeRequest
;
import
com.yaoai.admin.tenant.dto.TenantQuotaUpdateRequest
;
import
com.yaoai.admin.tenant.dto.TenantRechargeRequest
;
import
com.yaoai.admin.tenant.dto.TenantSuspendRequest
;
import
com.yaoai.admin.tenant.dto.TenantUpdateRequest
;
import
com.yaoai.common.result.Result
;
import
com.yaoai.domain.entity.Project
;
import
com.yaoai.domain.entity.Tenant
;
import
com.yaoai.domain.entity.TenantQuota
;
import
com.yaoai.domain.entity.User
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PatchMapping
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.PostMapping
;
import
org.springframework.web.bind.annotation.PutMapping
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
org.springframework.web.bind.annotation.RestController
;
import
java.util.List
;
@RestController
@RequestMapping
(
"/admin-api/tenants"
)
@RequiredArgsConstructor
@SaCheckLogin
(
type
=
"admin"
)
public
class
TenantAdminController
{
private
final
TenantAdminService
tenantAdminService
;
@GetMapping
public
Result
<
PageResult
<
TenantListItem
>>
list
(
@RequestParam
(
required
=
false
)
String
keyword
,
@RequestParam
(
required
=
false
)
String
plan
,
@RequestParam
(
required
=
false
)
Integer
status
,
@RequestParam
(
defaultValue
=
"1"
)
long
page
,
@RequestParam
(
defaultValue
=
"20"
)
long
size
)
{
return
Result
.
ok
(
tenantAdminService
.
list
(
keyword
,
plan
,
status
,
page
,
size
));
}
@GetMapping
(
"/{id}"
)
public
Result
<
TenantDetail
>
detail
(
@PathVariable
Long
id
)
{
return
Result
.
ok
(
tenantAdminService
.
detail
(
id
));
}
@PatchMapping
(
"/{id}"
)
@AdminAudited
(
action
=
"TENANT_UPDATE"
,
resource
=
"TENANT"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
Tenant
>
update
(
@PathVariable
Long
id
,
@RequestBody
TenantUpdateRequest
request
)
{
return
Result
.
ok
(
tenantAdminService
.
update
(
id
,
request
));
}
@PutMapping
(
"/{id}/plan"
)
@AdminAudited
(
action
=
"TENANT_PLAN_CHANGE"
,
resource
=
"TENANT"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
Tenant
>
changePlan
(
@PathVariable
Long
id
,
@RequestBody
TenantPlanChangeRequest
request
)
{
return
Result
.
ok
(
tenantAdminService
.
changePlan
(
id
,
request
));
}
@PostMapping
(
"/{id}/suspend"
)
@AdminAudited
(
action
=
"TENANT_SUSPEND"
,
resource
=
"TENANT"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
Tenant
>
suspend
(
@PathVariable
Long
id
,
@RequestBody
TenantSuspendRequest
request
)
{
return
Result
.
ok
(
tenantAdminService
.
suspend
(
id
,
request
));
}
@PostMapping
(
"/{id}/resume"
)
@AdminAudited
(
action
=
"TENANT_RESUME"
,
resource
=
"TENANT"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
Tenant
>
resume
(
@PathVariable
Long
id
)
{
return
Result
.
ok
(
tenantAdminService
.
resume
(
id
));
}
@GetMapping
(
"/{id}/balance"
)
public
Result
<
TenantBalanceOverview
>
balance
(
@PathVariable
Long
id
)
{
return
Result
.
ok
(
tenantAdminService
.
balance
(
id
));
}
@GetMapping
(
"/{id}/members"
)
public
Result
<
List
<
User
>>
members
(
@PathVariable
Long
id
)
{
return
Result
.
ok
(
tenantAdminService
.
members
(
id
));
}
@GetMapping
(
"/{id}/projects"
)
public
Result
<
List
<
Project
>>
projects
(
@PathVariable
Long
id
)
{
return
Result
.
ok
(
tenantAdminService
.
projects
(
id
));
}
@PostMapping
(
"/{id}/recharge"
)
@AdminAudited
(
action
=
"TENANT_RECHARGE"
,
resource
=
"TENANT"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"FINANCE"
},
mode
=
SaMode
.
OR
)
public
Result
<
TenantBalanceOverview
>
recharge
(
@PathVariable
Long
id
,
@RequestBody
TenantRechargeRequest
request
)
{
return
Result
.
ok
(
tenantAdminService
.
recharge
(
id
,
request
));
}
@PutMapping
(
"/{id}/quota"
)
@AdminAudited
(
action
=
"TENANT_QUOTA_UPDATE"
,
resource
=
"TENANT"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
})
public
Result
<
TenantQuota
>
updateQuota
(
@PathVariable
Long
id
,
@RequestBody
TenantQuotaUpdateRequest
request
)
{
return
Result
.
ok
(
tenantAdminService
.
updateQuota
(
id
,
request
));
}
@PostMapping
(
"/{id}/impersonate"
)
@AdminAudited
(
action
=
"IMPERSONATE_START"
,
resource
=
"TENANT"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
TenantImpersonateResponse
>
impersonate
(
@PathVariable
Long
id
,
@RequestBody
(
required
=
false
)
TenantImpersonateRequest
request
)
{
return
Result
.
ok
(
tenantAdminService
.
impersonate
(
id
,
request
==
null
?
new
TenantImpersonateRequest
()
:
request
));
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/TenantAdminService.java
0 → 100644
View file @
b7185a90
This diff is collapsed.
Click to expand it.
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantBalanceOverview.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
com.yaoai.domain.entity.BillingRecord
;
import
com.yaoai.domain.entity.TenantBalance
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.util.List
;
@Data
@Builder
public
class
TenantBalanceOverview
{
private
TenantBalance
balance
;
private
List
<
BillingRecord
>
recentRecords
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantDetail.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
com.yaoai.domain.entity.Tenant
;
import
com.yaoai.domain.entity.TenantBalance
;
import
com.yaoai.domain.entity.TenantQuota
;
import
lombok.Builder
;
import
lombok.Data
;
@Data
@Builder
public
class
TenantDetail
{
private
Tenant
tenant
;
private
TenantBalance
balance
;
private
TenantQuota
quota
;
private
long
memberCount
;
private
long
projectCount
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantImpersonateRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
lombok.Data
;
@Data
public
class
TenantImpersonateRequest
{
private
Long
userId
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantImpersonateResponse.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
@Data
@Builder
public
class
TenantImpersonateResponse
{
private
String
token
;
private
Long
tenantId
;
private
Long
userId
;
private
long
ttlSeconds
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantListItem.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.math.BigDecimal
;
import
java.time.LocalDateTime
;
@Data
@Builder
public
class
TenantListItem
{
private
Long
id
;
private
String
name
;
private
String
slug
;
private
String
plan
;
private
Integer
status
;
private
BigDecimal
balance
;
private
long
memberCount
;
private
long
projectCount
;
private
LocalDateTime
createdAt
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantPlanChangeRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
lombok.Data
;
@Data
public
class
TenantPlanChangeRequest
{
private
String
planCode
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantQuotaUpdateRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
lombok.Data
;
import
java.math.BigDecimal
;
@Data
public
class
TenantQuotaUpdateRequest
{
private
Integer
maxMembers
;
private
Integer
maxProjects
;
private
Integer
maxMonthlyCredits
;
private
Integer
maxConcurrentTasks
;
private
BigDecimal
monthlyBudgetLimit
;
private
String
notes
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantRechargeRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
lombok.Data
;
import
java.math.BigDecimal
;
@Data
public
class
TenantRechargeRequest
{
private
BigDecimal
amount
;
private
String
note
;
private
String
externalRefId
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantSuspendRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
lombok.Data
;
@Data
public
class
TenantSuspendRequest
{
private
String
reason
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/tenant/dto/TenantUpdateRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
tenant
.
dto
;
import
lombok.Data
;
@Data
public
class
TenantUpdateRequest
{
private
String
name
;
private
String
notes
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/user/UserAdminController.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
user
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
cn.dev33.satoken.annotation.SaCheckRole
;
import
cn.dev33.satoken.annotation.SaMode
;
import
com.yaoai.admin.audit.annotation.AdminAudited
;
import
com.yaoai.admin.common.PageResult
;
import
com.yaoai.admin.user.dto.ResetPasswordResponse
;
import
com.yaoai.admin.user.dto.UserQuery
;
import
com.yaoai.admin.user.dto.UserStatusRequest
;
import
com.yaoai.common.result.Result
;
import
com.yaoai.domain.entity.User
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.ModelAttribute
;
import
org.springframework.web.bind.annotation.PatchMapping
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.PostMapping
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RestController
;
@RestController
@RequestMapping
(
"/admin-api/users"
)
@RequiredArgsConstructor
@SaCheckLogin
(
type
=
"admin"
)
public
class
UserAdminController
{
private
final
UserAdminService
userAdminService
;
@GetMapping
public
Result
<
PageResult
<
User
>>
list
(
@ModelAttribute
UserQuery
query
)
{
return
Result
.
ok
(
userAdminService
.
list
(
query
));
}
@GetMapping
(
"/{id}"
)
public
Result
<
User
>
detail
(
@PathVariable
Long
id
)
{
return
Result
.
ok
(
userAdminService
.
detail
(
id
));
}
@PatchMapping
(
"/{id}/status"
)
@AdminAudited
(
action
=
"USER_STATUS_CHANGE"
,
resource
=
"USER"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
User
>
updateStatus
(
@PathVariable
Long
id
,
@RequestBody
UserStatusRequest
request
)
{
return
Result
.
ok
(
userAdminService
.
updateStatus
(
id
,
request
));
}
@PostMapping
(
"/{id}/kickout"
)
@AdminAudited
(
action
=
"USER_KICKOUT"
,
resource
=
"USER"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
Void
>
kickout
(
@PathVariable
Long
id
)
{
userAdminService
.
kickout
(
id
);
return
Result
.
ok
();
}
@PostMapping
(
"/{id}/reset-password"
)
@AdminAudited
(
action
=
"USER_RESET_PASSWORD"
,
resource
=
"USER"
,
resourceIdParam
=
"id"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
ResetPasswordResponse
>
resetPassword
(
@PathVariable
Long
id
)
{
return
Result
.
ok
(
userAdminService
.
resetPassword
(
id
));
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/user/UserAdminService.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
user
;
import
cn.dev33.satoken.stp.StpUtil
;
import
cn.hutool.core.util.RandomUtil
;
import
cn.hutool.crypto.digest.BCrypt
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.core.toolkit.Wrappers
;
import
com.yaoai.admin.common.PageResult
;
import
com.yaoai.admin.user.dto.ResetPasswordResponse
;
import
com.yaoai.admin.user.dto.UserQuery
;
import
com.yaoai.admin.user.dto.UserStatusRequest
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.User
;
import
com.yaoai.domain.mapper.UserMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
org.springframework.util.StringUtils
;
import
java.time.LocalDateTime
;
@Service
@RequiredArgsConstructor
public
class
UserAdminService
{
private
final
UserMapper
userMapper
;
public
PageResult
<
User
>
list
(
UserQuery
query
)
{
long
page
=
Math
.
max
(
1
,
query
.
getPage
());
long
size
=
Math
.
max
(
1
,
Math
.
min
(
query
.
getSize
(),
100
));
long
offset
=
(
page
-
1
)
*
size
;
long
total
=
userMapper
.
selectCount
(
buildQuery
(
query
));
return
new
PageResult
<>(
userMapper
.
selectList
(
buildQuery
(
query
)
.
orderByDesc
(
User:
:
getCreatedAt
)
.
last
(
"LIMIT "
+
size
+
" OFFSET "
+
offset
)),
total
,
page
,
size
);
}
public
User
detail
(
Long
id
)
{
return
requireUser
(
id
);
}
@Transactional
public
User
updateStatus
(
Long
id
,
UserStatusRequest
request
)
{
User
user
=
requireUser
(
id
);
if
(
request
.
getStatus
()
==
null
||
(
request
.
getStatus
()
!=
0
&&
request
.
getStatus
()
!=
1
))
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"用户状态必须为 0 或 1"
);
}
user
.
setStatus
(
request
.
getStatus
());
user
.
setUpdatedAt
(
LocalDateTime
.
now
());
userMapper
.
updateById
(
user
);
if
(
request
.
getStatus
()
==
0
)
{
StpUtil
.
kickout
(
id
);
}
return
userMapper
.
selectById
(
id
);
}
public
void
kickout
(
Long
id
)
{
requireUser
(
id
);
StpUtil
.
kickout
(
id
);
}
@Transactional
public
ResetPasswordResponse
resetPassword
(
Long
id
)
{
User
user
=
requireUser
(
id
);
String
password
=
"YaoAI-"
+
RandomUtil
.
randomString
(
10
);
user
.
setPasswordHash
(
BCrypt
.
hashpw
(
password
,
BCrypt
.
gensalt
()));
user
.
setUpdatedAt
(
LocalDateTime
.
now
());
userMapper
.
updateById
(
user
);
StpUtil
.
kickout
(
id
);
return
new
ResetPasswordResponse
(
password
);
}
private
LambdaQueryWrapper
<
User
>
buildQuery
(
UserQuery
query
)
{
LambdaQueryWrapper
<
User
>
wrapper
=
Wrappers
.<
User
>
lambdaQuery
()
.
isNull
(
User:
:
getDeletedAt
);
if
(
query
.
getTenantId
()
!=
null
)
{
wrapper
.
eq
(
User:
:
getTenantId
,
query
.
getTenantId
());
}
if
(
query
.
getStatus
()
!=
null
)
{
wrapper
.
eq
(
User:
:
getStatus
,
query
.
getStatus
());
}
if
(
StringUtils
.
hasText
(
query
.
getKeyword
()))
{
wrapper
.
and
(
w
->
w
.
like
(
User:
:
getEmail
,
query
.
getKeyword
())
.
or
()
.
like
(
User:
:
getUsername
,
query
.
getKeyword
())
.
or
()
.
like
(
User:
:
getPhone
,
query
.
getKeyword
()));
}
return
wrapper
;
}
private
User
requireUser
(
Long
id
)
{
User
user
=
userMapper
.
selectOne
(
Wrappers
.<
User
>
lambdaQuery
()
.
eq
(
User:
:
getId
,
id
)
.
isNull
(
User:
:
getDeletedAt
));
if
(
user
==
null
)
{
throw
new
BizException
(
ErrorCode
.
NOT_FOUND
,
"用户不存在"
);
}
return
user
;
}
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/user/dto/ResetPasswordResponse.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
user
.
dto
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
@Data
@AllArgsConstructor
public
class
ResetPasswordResponse
{
private
String
password
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/user/dto/UserQuery.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
user
.
dto
;
import
lombok.Data
;
@Data
public
class
UserQuery
{
private
Long
tenantId
;
private
String
keyword
;
private
Integer
status
;
private
long
page
=
1
;
private
long
size
=
20
;
}
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/user/dto/UserStatusRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
admin
.
user
.
dto
;
import
lombok.Data
;
@Data
public
class
UserStatusRequest
{
private
Integer
status
;
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/util/SeedancePromptBuilder.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
ai
.
providers
.
util
;
import
java.util.ArrayList
;
import
java.util.List
;
/**
* 把"结构化视频生成请求"拼装为 Seedance 2.0 可消费的 final prompt + 图片顺序。
*
* 图片顺序固定(即 @图N 标号):
* @图1..@图K = 角色(K = 角色数量),@图K+1 = 场景,
* @图K+2.. = 道具(按顺序),最后一张 = 风格图(如有)
*/
public
final
class
SeedancePromptBuilder
{
private
SeedancePromptBuilder
()
{}
/**
* 仅依赖原语类型(字符串),不引入 yaoai-api DTO,保持模块依赖单向。
*
* @param characterCount 角色图数量(>=1,决定 @图1..@图K 的范围)
* @param propCount 道具图数量(用于计算 @图N 的范围)
* @param hasStyleImage 是否含风格图
*/
/** V17:单段 freeform 提示词模式(前端单输入框后的主要入口)。 */
public
static
String
buildPromptFreeform
(
String
freeformBody
,
int
characterCount
,
int
propCount
,
boolean
hasStyleImage
)
{
StringBuilder
sb
=
new
StringBuilder
();
appendImageHeader
(
sb
,
characterCount
,
propCount
,
hasStyleImage
);
sb
.
append
(
"\n分镜内容:\n"
);
if
(
freeformBody
!=
null
&&
!
freeformBody
.
isBlank
())
{
sb
.
append
(
freeformBody
.
trim
()).
append
(
'\n'
);
}
appendCommonRules
(
sb
);
return
sb
.
toString
();
}
public
static
String
buildPrompt
(
UserPromptParts
parts
,
int
characterCount
,
int
propCount
,
boolean
hasStyleImage
)
{
StringBuilder
sb
=
new
StringBuilder
();
appendImageHeader
(
sb
,
characterCount
,
propCount
,
hasStyleImage
);
sb
.
append
(
"\n分镜内容:\n"
);
appendIfPresent
(
sb
,
"人物动作"
,
parts
.
characterAction
());
appendIfPresent
(
sb
,
"场景事件"
,
parts
.
sceneEvent
());
appendIfPresent
(
sb
,
"镜头运动"
,
parts
.
cameraMovement
());
appendIfPresent
(
sb
,
"光影氛围"
,
parts
.
lightingAtmosphere
());
appendIfPresent
(
sb
,
"视频风格"
,
parts
.
videoStyle
());
appendCommonRules
(
sb
);
return
sb
.
toString
();
}
private
static
void
appendImageHeader
(
StringBuilder
sb
,
int
characterCount
,
int
propCount
,
boolean
hasStyleImage
)
{
int
imageCount
;
if
(
characterCount
<=
1
)
{
sb
.
append
(
"@图1 是角色参考图,请保持人物脸型、发型、服装、气质一致,不要随意改变角色身份。\n"
);
imageCount
=
1
;
}
else
{
sb
.
append
(
"@图1-"
).
append
(
characterCount
)
.
append
(
" 是出镜角色参考图(按顺序对应剧本中第 1.."
).
append
(
characterCount
)
.
append
(
" 位角色),请保持每位人物的脸型、发型、服装、气质一致,不要混淆角色身份。\n"
);
imageCount
=
characterCount
;
}
sb
.
append
(
"@图"
).
append
(
imageCount
+
1
)
.
append
(
" 是场景参考图,请参考空间结构、环境布局、光影、氛围和构图。\n"
);
imageCount
+=
1
;
if
(
propCount
>
0
)
{
int
propStart
=
imageCount
+
1
;
int
propEnd
=
propStart
+
propCount
-
1
;
if
(
propCount
==
1
)
{
sb
.
append
(
"@图"
).
append
(
propStart
).
append
(
" 是道具参考图,请在画面中合理使用。\n"
);
}
else
{
sb
.
append
(
"@图"
).
append
(
propStart
).
append
(
"-"
).
append
(
propEnd
)
.
append
(
" 是道具参考图,请在画面中合理使用这些道具。\n"
);
}
imageCount
+=
propCount
;
}
if
(
hasStyleImage
)
{
sb
.
append
(
"@图"
).
append
(
imageCount
+
1
)
.
append
(
" 是风格参考图,请参考整体美术风格、色彩和质感。\n"
);
}
}
private
static
void
appendCommonRules
(
StringBuilder
sb
)
{
sb
.
append
(
"\n生成要求:\n"
);
sb
.
append
(
"- 保持角色一致性\n"
);
sb
.
append
(
"- 保持场景逻辑合理\n"
);
sb
.
append
(
"- 人物与环境融合自然\n"
);
sb
.
append
(
"- 镜头运动流畅\n"
);
sb
.
append
(
"- 画面具备电影感\n"
);
sb
.
append
(
"- 不要出现多余人物\n"
);
sb
.
append
(
"- 不要出现明显变脸、错位、肢体畸形\n"
);
}
/**
* 按 @图N 顺序拼接图片 TOS key 列表(角色1..K → 场景 → 道具… → 风格)。
*
* @param characterImageKeys 出镜角色图 keys(>=1 张)
*/
public
static
List
<
String
>
buildOrderedImageKeys
(
List
<
String
>
characterImageKeys
,
String
sceneImageKey
,
List
<
String
>
propImageKeys
,
String
styleImageKey
)
{
List
<
String
>
keys
=
new
ArrayList
<>();
if
(
characterImageKeys
!=
null
)
{
for
(
String
k
:
characterImageKeys
)
{
if
(
k
!=
null
&&
!
k
.
isBlank
())
{
keys
.
add
(
k
);
}
}
}
keys
.
add
(
sceneImageKey
);
if
(
propImageKeys
!=
null
)
{
for
(
String
k
:
propImageKeys
)
{
if
(
k
!=
null
&&
!
k
.
isBlank
())
{
keys
.
add
(
k
);
}
}
}
if
(
styleImageKey
!=
null
&&
!
styleImageKey
.
isBlank
())
{
keys
.
add
(
styleImageKey
);
}
return
keys
;
}
private
static
void
appendIfPresent
(
StringBuilder
sb
,
String
label
,
String
value
)
{
if
(
value
!=
null
&&
!
value
.
isBlank
())
{
sb
.
append
(
"- "
).
append
(
label
).
append
(
":"
).
append
(
value
.
trim
()).
append
(
'\n'
);
}
}
/** 用户结构化提示词的五段输入;放在工具类内部避免跨模块耦合。 */
public
record
UserPromptParts
(
String
characterAction
,
String
sceneEvent
,
String
cameraMovement
,
String
lightingAtmosphere
,
String
videoStyle
)
{
}
}
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/StructuredVideoGenerateRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
api
.
dto
.
ai
;
import
jakarta.validation.Valid
;
import
jakarta.validation.constraints.Max
;
import
jakarta.validation.constraints.Min
;
import
jakarta.validation.constraints.NotBlank
;
import
jakarta.validation.constraints.NotNull
;
import
jakarta.validation.constraints.Pattern
;
import
jakarta.validation.constraints.Size
;
import
lombok.Data
;
import
java.util.List
;
/**
* 分镜视频结构化生成请求。
* 替代旧的「prompt 内夹 @图N + image_keys[] 顺序绑定」方式。
*/
@Data
public
class
StructuredVideoGenerateRequest
{
/** 关联剧集(可选) */
private
Long
episodeId
;
/** 关联分镜(可选) */
private
Long
storyboardId
;
/** @图1 角色图(旧版单角色字段,characterImageKeys 为空时使用) */
private
String
characterImageKey
;
/** V16:多角色出镜(@图1..@图N,按顺序),与 characterImageKey 至少传一个 */
@Size
(
max
=
5
,
message
=
"出镜角色最多 5 个"
)
private
List
<
String
>
characterImageKeys
;
/** @图2 场景图(必填) */
@NotBlank
(
message
=
"场景图不能为空"
)
private
String
sceneImageKey
;
/** 用户结构化提示词(旧版 5 段,与 freeformPrompt 二选一,至少一项非空) */
@Valid
private
UserPrompt
userPrompt
;
/** V17:单段 freeform 提示词(前端简化为单输入框后的主要入口) */
private
String
freeformPrompt
;
/** @图3+ 道具图(可选,按顺序对应 @图3、@图4...,最多 5 张) */
@Size
(
max
=
5
,
message
=
"道具图最多 5 张"
)
private
List
<
String
>
propImageKeys
;
/** 风格图(可选,固定为最后一张) */
private
String
styleImageKey
;
/** 视频时长(秒) */
@Min
(
value
=
5
,
message
=
"视频时长最短 5 秒"
)
@Max
(
value
=
30
,
message
=
"视频时长最长 30 秒"
)
private
Integer
duration
=
15
;
/** 16:9 / 9:16 / 1:1 */
@Pattern
(
regexp
=
"16:9|9:16|1:1"
,
message
=
"宽高比仅支持 16:9 / 9:16 / 1:1"
)
private
String
ratio
=
"16:9"
;
/** Seedance 模型 ID(doubao-seedance-2-0-260128 / doubao-seedance-2-0-fast-260128),为空走 ArkProperties 默认值 */
@Pattern
(
regexp
=
"doubao-seedance-2-0-260128|doubao-seedance-2-0-fast-260128"
,
message
=
"模型仅支持 Seedance 2.0 标准 / 极速"
)
private
String
model
;
/** 是否生成配音 */
private
Boolean
generateAudio
=
Boolean
.
FALSE
;
}
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/UserPrompt.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
api
.
dto
.
ai
;
import
lombok.Data
;
/**
* 用户在分镜页输入的结构化提示词。
* 拆分为五段,避免用户写 @图N 自然语言占位。
*/
@Data
public
class
UserPrompt
{
/** 人物动作 */
private
String
characterAction
;
/** 场景事件 */
private
String
sceneEvent
;
/** 镜头运动 */
private
String
cameraMovement
;
/** 光影氛围 */
private
String
lightingAtmosphere
;
/** 视频风格 */
private
String
videoStyle
;
/** 至少填一项 */
public
boolean
hasAnyContent
()
{
return
notBlank
(
characterAction
)
||
notBlank
(
sceneEvent
)
||
notBlank
(
cameraMovement
)
||
notBlank
(
lightingAtmosphere
)
||
notBlank
(
videoStyle
);
}
private
static
boolean
notBlank
(
String
s
)
{
return
s
!=
null
&&
!
s
.
isBlank
();
}
}
yaoai-comic-studio/yaoai-billing/src/main/java/com/yaoai/billing/dto/BillingChargeRequest.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
billing
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.math.BigDecimal
;
import
java.util.Map
;
@Data
@Builder
public
class
BillingChargeRequest
{
private
Long
tenantId
;
private
Long
userId
;
private
Long
projectId
;
/** outline_generate / episode_generate / image_generate / video_generate / assembly */
private
String
operation
;
/** TEXT / IMAGE / VIDEO / AUDIO / COMPOSE */
private
String
modality
;
/** volcengine / qwen / openai / ffmpeg */
private
String
modelProvider
;
/** 模型唯一 ID */
private
String
modelId
;
/** 模型展示名 */
private
String
modelName
;
/** 单次操作的单元数 */
private
Integer
unitCount
;
/** 是否成功;失败时记 credits=0 */
private
Boolean
success
;
/** 显式覆盖 BillingConfig 计算的费用,null 时按 operation 查表 */
private
BigDecimal
credits
;
/** 任意元信息:tokenCount / size / duration */
private
Map
<
String
,
Object
>
meta
;
/** task ID / project ID */
private
String
refId
;
}
yaoai-comic-studio/yaoai-billing/src/main/java/com/yaoai/billing/service/BillingCostService.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
billing
.
service
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.billing.config.BillingConfig
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.SystemSetting
;
import
com.yaoai.domain.mapper.SystemSettingMapper
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.cache.annotation.CacheEvict
;
import
org.springframework.cache.annotation.Cacheable
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
java.math.BigDecimal
;
import
java.time.LocalDateTime
;
import
java.util.LinkedHashMap
;
import
java.util.Map
;
@Service
@RequiredArgsConstructor
public
class
BillingCostService
{
public
static
final
String
SETTING_KEY
=
"billing.costs"
;
private
final
BillingConfig
billingConfig
;
private
final
SystemSettingMapper
systemSettingMapper
;
private
final
ObjectMapper
objectMapper
;
@Cacheable
(
value
=
"billing-costs"
,
key
=
"#operation"
)
public
BigDecimal
getCost
(
String
operation
)
{
return
getCosts
().
getOrDefault
(
operation
,
BigDecimal
.
ZERO
);
}
@Cacheable
(
value
=
"billing-costs"
,
key
=
"'global'"
)
public
Map
<
String
,
BigDecimal
>
getCosts
()
{
Map
<
String
,
BigDecimal
>
costs
=
new
LinkedHashMap
<>(
billingConfig
.
getCosts
());
SystemSetting
setting
=
systemSettingMapper
.
findByKey
(
SETTING_KEY
).
orElse
(
null
);
if
(
setting
==
null
||
setting
.
getSettingValue
()
==
null
||
setting
.
getSettingValue
().
isBlank
())
{
return
costs
;
}
try
{
Map
<
String
,
BigDecimal
>
overrides
=
objectMapper
.
readValue
(
setting
.
getSettingValue
(),
new
TypeReference
<>()
{
}
);
overrides
.
forEach
((
operation
,
cost
)
->
{
if
(
operation
!=
null
&&
cost
!=
null
)
{
costs
.
put
(
operation
,
cost
);
}
});
return
costs
;
}
catch
(
Exception
e
)
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"计费价格配置解析失败: "
+
e
.
getMessage
());
}
}
@Transactional
@CacheEvict
(
value
=
"billing-costs"
,
allEntries
=
true
)
public
Map
<
String
,
BigDecimal
>
saveCosts
(
Map
<
String
,
BigDecimal
>
costs
,
Long
adminId
)
{
Map
<
String
,
BigDecimal
>
normalized
=
normalize
(
costs
);
try
{
String
json
=
objectMapper
.
writeValueAsString
(
normalized
);
SystemSetting
setting
=
systemSettingMapper
.
findByKey
(
SETTING_KEY
).
orElse
(
null
);
if
(
setting
==
null
)
{
setting
=
new
SystemSetting
();
setting
.
setSettingKey
(
SETTING_KEY
);
setting
.
setSettingValue
(
json
);
setting
.
setUpdatedAt
(
LocalDateTime
.
now
());
setting
.
setUpdatedBy
(
adminId
);
systemSettingMapper
.
insert
(
setting
);
}
else
{
setting
.
setSettingValue
(
json
);
setting
.
setUpdatedAt
(
LocalDateTime
.
now
());
setting
.
setUpdatedBy
(
adminId
);
systemSettingMapper
.
updateById
(
setting
);
}
return
normalized
;
}
catch
(
Exception
e
)
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"计费价格配置保存失败: "
+
e
.
getMessage
());
}
}
private
Map
<
String
,
BigDecimal
>
normalize
(
Map
<
String
,
BigDecimal
>
costs
)
{
if
(
costs
==
null
||
costs
.
isEmpty
())
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"计费价格不能为空"
);
}
Map
<
String
,
BigDecimal
>
normalized
=
new
LinkedHashMap
<>();
costs
.
forEach
((
operation
,
cost
)
->
{
if
(
operation
==
null
||
operation
.
isBlank
())
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"计费操作不能为空"
);
}
if
(
cost
==
null
||
cost
.
compareTo
(
BigDecimal
.
ZERO
)
<
0
)
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"计费价格不能小于 0"
);
}
normalized
.
put
(
operation
.
trim
(),
cost
);
});
return
normalized
;
}
}
yaoai-comic-studio/yaoai-bootstrap/src/main/resources/db/migration/V13__admin_backend.sql
0 → 100644
View file @
b7185a90
-- ============================================================
-- M14 平台运营后台
-- 1) admin_users / admin_audit_logs / plans / tenant_quotas
-- 2) tenants 加暂停元信息
-- 3) billing_records 升级到模型粒度(核心)
-- 4) 套餐初始化数据
-- ============================================================
-- ---------- 1) 运营人员 ----------
CREATE
TABLE
admin_users
(
id
BIGINT
NOT
NULL
,
username
VARCHAR
(
64
)
NOT
NULL
,
password_hash
VARCHAR
(
255
)
DEFAULT
NULL
COMMENT
'BCrypt; 首启由 AdminInitializer 注入默认密码'
,
display_name
VARCHAR
(
128
)
DEFAULT
NULL
,
email
VARCHAR
(
255
)
DEFAULT
NULL
,
role
VARCHAR
(
32
)
NOT
NULL
DEFAULT
'OPERATOR'
COMMENT
'SUPER_ADMIN/OPERATOR/FINANCE/READONLY'
,
status
TINYINT
NOT
NULL
DEFAULT
1
COMMENT
'1=ACTIVE 0=DISABLED'
,
must_change_password
TINYINT
NOT
NULL
DEFAULT
0
,
last_login_at
DATETIME
(
3
)
DEFAULT
NULL
,
last_login_ip
VARCHAR
(
45
)
DEFAULT
NULL
,
created_by
BIGINT
DEFAULT
NULL
,
created_at
DATETIME
(
3
)
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
(
3
),
updated_at
DATETIME
(
3
)
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
(
3
)
ON
UPDATE
CURRENT_TIMESTAMP
(
3
),
PRIMARY
KEY
(
id
),
UNIQUE
KEY
uk_username
(
username
),
KEY
idx_role_status
(
role
,
status
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
'运营人员'
;
-- ---------- 2) 运营操作审计 ----------
CREATE
TABLE
admin_audit_logs
(
id
BIGINT
NOT
NULL
,
admin_user_id
BIGINT
NOT
NULL
,
admin_username
VARCHAR
(
64
)
DEFAULT
NULL
COMMENT
'冗余字段,账号删除后仍可追溯'
,
action
VARCHAR
(
64
)
NOT
NULL
COMMENT
'TENANT_RECHARGE/TENANT_SUSPEND/PLAN_CHANGE/USER_DISABLE/IMPERSONATE_START/...'
,
resource_type
VARCHAR
(
32
)
DEFAULT
NULL
,
resource_id
VARCHAR
(
64
)
DEFAULT
NULL
,
before_value
JSON
DEFAULT
NULL
,
after_value
JSON
DEFAULT
NULL
,
ip
VARCHAR
(
45
)
DEFAULT
NULL
,
user_agent
VARCHAR
(
255
)
DEFAULT
NULL
,
request_id
VARCHAR
(
64
)
DEFAULT
NULL
,
status
VARCHAR
(
16
)
NOT
NULL
DEFAULT
'SUCCESS'
COMMENT
'SUCCESS/FAILED'
,
error_message
TEXT
DEFAULT
NULL
,
created_at
DATETIME
(
3
)
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
(
3
),
PRIMARY
KEY
(
id
),
KEY
idx_admin_time
(
admin_user_id
,
created_at
),
KEY
idx_resource
(
resource_type
,
resource_id
),
KEY
idx_action_time
(
action
,
created_at
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
'运营操作审计'
;
-- ---------- 3) 套餐定义 ----------
CREATE
TABLE
plans
(
id
BIGINT
NOT
NULL
,
code
VARCHAR
(
32
)
NOT
NULL
COMMENT
'FREE/PRO/TEAM/ENTERPRISE'
,
name
VARCHAR
(
64
)
NOT
NULL
,
price_monthly
DECIMAL
(
10
,
2
)
DEFAULT
0
,
monthly_credits
INT
DEFAULT
0
,
max_members
INT
DEFAULT
1
COMMENT
'0=不限'
,
max_projects
INT
DEFAULT
0
COMMENT
'0=不限'
,
max_concurrent_tasks
INT
DEFAULT
1
,
features
JSON
DEFAULT
NULL
,
is_active
TINYINT
NOT
NULL
DEFAULT
1
,
updated_at
DATETIME
(
3
)
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
(
3
)
ON
UPDATE
CURRENT_TIMESTAMP
(
3
),
updated_by
BIGINT
DEFAULT
NULL
,
PRIMARY
KEY
(
id
),
UNIQUE
KEY
uk_code
(
code
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
'套餐定义'
;
-- ---------- 4) 租户配额覆盖 ----------
CREATE
TABLE
tenant_quotas
(
tenant_id
BIGINT
NOT
NULL
,
max_members
INT
DEFAULT
NULL
,
max_projects
INT
DEFAULT
NULL
,
max_monthly_credits
INT
DEFAULT
NULL
,
max_concurrent_tasks
INT
DEFAULT
NULL
,
monthly_budget_limit
DECIMAL
(
18
,
4
)
DEFAULT
NULL
,
notes
TEXT
DEFAULT
NULL
,
updated_at
DATETIME
(
3
)
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
(
3
)
ON
UPDATE
CURRENT_TIMESTAMP
(
3
),
updated_by
BIGINT
DEFAULT
NULL
,
PRIMARY
KEY
(
tenant_id
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
'租户配额覆盖'
;
-- ---------- 5) tenants 暂停元信息 ----------
ALTER
TABLE
tenants
ADD
COLUMN
suspended_at
DATETIME
(
3
)
DEFAULT
NULL
COMMENT
'暂停时间'
,
ADD
COLUMN
suspended_by
BIGINT
DEFAULT
NULL
COMMENT
'操作人 admin_users.id'
,
ADD
COLUMN
suspended_reason
VARCHAR
(
255
)
DEFAULT
NULL
,
ADD
COLUMN
notes
TEXT
DEFAULT
NULL
COMMENT
'运营备注'
;
-- ---------- 6) billing_records 模型粒度升级(核心) ----------
ALTER
TABLE
billing_records
ADD
COLUMN
created_by_admin_id
BIGINT
DEFAULT
NULL
COMMENT
'运营手动充值时记录'
,
ADD
COLUMN
user_id
BIGINT
DEFAULT
NULL
COMMENT
'租户内发起操作的用户'
,
ADD
COLUMN
modality
VARCHAR
(
16
)
DEFAULT
NULL
COMMENT
'TEXT/IMAGE/VIDEO/AUDIO/COMPOSE/RECHARGE'
,
ADD
COLUMN
model_provider
VARCHAR
(
32
)
DEFAULT
NULL
COMMENT
'volcengine/qwen/openai/ffmpeg'
,
ADD
COLUMN
model_id
VARCHAR
(
64
)
DEFAULT
NULL
,
ADD
COLUMN
model_name
VARCHAR
(
128
)
DEFAULT
NULL
,
ADD
COLUMN
unit_count
INT
DEFAULT
1
,
ADD
COLUMN
success
TINYINT
(
1
)
DEFAULT
1
,
ADD
COLUMN
billing_meta
JSON
DEFAULT
NULL
,
ADD
INDEX
idx_modality_model
(
modality
,
model_provider
,
model_id
),
ADD
INDEX
idx_user_created
(
user_id
,
created_at
);
-- 把历史数据的 modality 推断填充(旧数据没有 modality 列,按 operation 反推)
UPDATE
billing_records
SET
modality
=
'TEXT'
WHERE
modality
IS
NULL
AND
operation
IN
(
'outline_generate'
,
'episode_generate'
);
UPDATE
billing_records
SET
modality
=
'IMAGE'
WHERE
modality
IS
NULL
AND
operation
=
'image_generate'
;
UPDATE
billing_records
SET
modality
=
'VIDEO'
WHERE
modality
IS
NULL
AND
operation
=
'video_generate'
;
UPDATE
billing_records
SET
modality
=
'COMPOSE'
WHERE
modality
IS
NULL
AND
operation
=
'assembly'
;
UPDATE
billing_records
SET
modality
=
'RECHARGE'
WHERE
modality
IS
NULL
AND
operation
=
'recharge'
;
-- ---------- 7) 套餐初始化数据(对齐 design-docs/01 §4.1) ----------
INSERT
INTO
plans
(
id
,
code
,
name
,
price_monthly
,
monthly_credits
,
max_members
,
max_projects
,
max_concurrent_tasks
,
features
,
is_active
)
VALUES
(
1
,
'FREE'
,
'免费版'
,
0
.
00
,
100
,
1
,
3
,
1
,
JSON_OBJECT
(
'watermark'
,
true
,
'libraries'
,
JSON_ARRAY
(
'personal'
)),
1
),
(
2
,
'PRO'
,
'专业版'
,
99
.
00
,
3000
,
5
,
20
,
3
,
JSON_OBJECT
(
'watermark'
,
false
,
'libraries'
,
JSON_ARRAY
(
'personal'
,
'team'
)),
1
),
(
3
,
'TEAM'
,
'团队版'
,
499
.
00
,
20000
,
20
,
100
,
5
,
JSON_OBJECT
(
'watermark'
,
false
,
'libraries'
,
JSON_ARRAY
(
'personal'
,
'team'
),
'api'
,
true
),
1
),
(
4
,
'ENTERPRISE'
,
'企业版'
,
0
.
00
,
0
,
0
,
0
,
10
,
JSON_OBJECT
(
'watermark'
,
false
,
'libraries'
,
JSON_ARRAY
(
'personal'
,
'team'
,
'market'
),
'api'
,
true
,
'sla'
,
true
,
'private_deploy'
,
true
),
1
);
yaoai-comic-studio/yaoai-bootstrap/src/main/resources/db/migration/V14__system_settings.sql
0 → 100644
View file @
b7185a90
CREATE
TABLE
system_settings
(
id
BIGINT
NOT
NULL
,
setting_key
VARCHAR
(
128
)
NOT
NULL
,
setting_value
TEXT
,
updated_at
DATETIME
(
3
)
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
(
3
)
ON
UPDATE
CURRENT_TIMESTAMP
(
3
),
updated_by
BIGINT
DEFAULT
NULL
,
PRIMARY
KEY
(
id
),
UNIQUE
KEY
uk_system_setting_key
(
setting_key
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COLLATE
=
utf8mb4_unicode_ci
COMMENT
=
'Platform-level key-value settings'
;
yaoai-comic-studio/yaoai-bootstrap/src/main/resources/db/migration/V15__extend_ai_tasks_structured.sql
0 → 100644
View file @
b7185a90
-- ============================================================
-- V14 结构化视频生成
-- 扩展 ai_tasks 表:把"@图1 + 自然语言"调用方式
-- 升级为"左图 + 右提示词"结构化调用
--
-- 兼容性约定:
-- - 旧字段 prompt 复用为「最终提交给 Seedance 的完整 Prompt」(含 @图N)
-- - 旧字段 input_image_key 仅旧任务使用,新结构化任务为 NULL
-- - 新结构化任务 task_type='structured_video'
-- ============================================================
ALTER
TABLE
ai_tasks
ADD
COLUMN
character_image_key
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'角色图 TOS key(@图1)'
,
ADD
COLUMN
scene_image_key
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'场景图 TOS key(@图2)'
,
ADD
COLUMN
prop_image_keys
TEXT
DEFAULT
NULL
COMMENT
'道具图 TOS keys(JSON 数组字符串,@图3+)'
,
ADD
COLUMN
style_image_key
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'风格图 TOS key(可选,最后一张)'
,
ADD
COLUMN
user_prompt
TEXT
DEFAULT
NULL
COMMENT
'用户原始结构化输入(JSON 字符串,与 prompt 字段配对存证)'
,
ADD
COLUMN
video_duration
INT
DEFAULT
NULL
COMMENT
'视频时长(秒)'
,
ADD
COLUMN
video_ratio
VARCHAR
(
10
)
DEFAULT
NULL
COMMENT
'视频宽高比 16:9/9:16/1:1'
,
ADD
COLUMN
generate_audio
TINYINT
(
1
)
NOT
NULL
DEFAULT
0
COMMENT
'是否生成配音 0/1'
;
-- 便于运营回溯:按角色/场景维度统计任务
CREATE
INDEX
idx_ai_tasks_character
ON
ai_tasks
(
character_image_key
);
CREATE
INDEX
idx_ai_tasks_scene
ON
ai_tasks
(
scene_image_key
);
yaoai-comic-studio/yaoai-bootstrap/src/main/resources/db/migration/V16__multi_character_per_storyboard.sql
0 → 100644
View file @
b7185a90
-- ============================================================
-- V16 多角色出镜支持
-- 每个分镜可携带多个角色变体(如:陆长生-白衣 / 陆长生-黑衣)
--
-- 兼容性约定:
-- - 保留 ai_tasks.character_image_key、storyboards 的旧字段不变
-- - 新增 character_image_keys(JSON 数组),新版结构化任务优先使用
-- - 旧任务字段保留只读,新写入只走数组列
-- ============================================================
ALTER
TABLE
ai_tasks
ADD
COLUMN
character_image_keys
TEXT
DEFAULT
NULL
COMMENT
'出镜角色图 TOS keys(JSON 数组字符串,@图1..@图N,按顺序排列)'
;
ALTER
TABLE
storyboards
ADD
COLUMN
character_image_keys
TEXT
DEFAULT
NULL
COMMENT
'该分镜的出镜角色图 TOS keys(JSON 数组字符串)'
,
ADD
COLUMN
scene_image_key
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'该分镜选中的场景图 TOS key'
,
ADD
COLUMN
prop_image_keys
TEXT
DEFAULT
NULL
COMMENT
'该分镜选中的道具图 TOS keys(JSON 数组字符串)'
,
ADD
COLUMN
style_image_key
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'该分镜选中的风格图 TOS key'
;
yaoai-comic-studio/yaoai-common/src/main/java/com/yaoai/common/result/Result.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
common
.
result
;
import
lombok.Data
;
import
org.slf4j.MDC
;
@Data
public
class
Result
<
T
>
{
private
boolean
success
;
private
T
data
;
private
ErrorDetail
error
;
private
String
requestId
;
private
long
timestamp
;
@Data
public
static
class
ErrorDetail
{
private
final
String
code
;
private
final
String
message
;
}
private
Result
()
{
this
.
requestId
=
MDC
.
get
(
"requestId"
);
this
.
timestamp
=
System
.
currentTimeMillis
();
}
public
static
<
T
>
Result
<
T
>
ok
(
T
data
)
{
Result
<
T
>
result
=
new
Result
<>();
result
.
success
=
true
;
result
.
data
=
data
;
return
result
;
}
public
static
<
T
>
Result
<
T
>
ok
()
{
return
ok
(
null
);
}
public
static
<
T
>
Result
<
T
>
error
(
String
code
,
String
message
)
{
Result
<
T
>
result
=
new
Result
<>();
result
.
success
=
false
;
result
.
error
=
new
ErrorDetail
(
code
,
message
);
return
result
;
}
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/AdminAuditLog.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
entity
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
lombok.Data
;
import
java.time.LocalDateTime
;
@Data
@TableName
(
"admin_audit_logs"
)
public
class
AdminAuditLog
{
@TableId
(
type
=
IdType
.
ASSIGN_ID
)
private
Long
id
;
private
Long
adminUserId
;
/** Redundant for traceability after account deletion. */
private
String
adminUsername
;
/** TENANT_RECHARGE / TENANT_SUSPEND / PLAN_CHANGE / USER_DISABLE / IMPERSONATE_START / ... */
private
String
action
;
private
String
resourceType
;
private
String
resourceId
;
/** JSON */
private
String
beforeValue
;
/** JSON */
private
String
afterValue
;
private
String
ip
;
private
String
userAgent
;
private
String
requestId
;
/** SUCCESS / FAILED */
private
String
status
;
private
String
errorMessage
;
private
LocalDateTime
createdAt
;
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/AdminUser.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
entity
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
com.fasterxml.jackson.annotation.JsonIgnore
;
import
lombok.Data
;
import
java.time.LocalDateTime
;
@Data
@TableName
(
"admin_users"
)
public
class
AdminUser
{
@TableId
(
type
=
IdType
.
ASSIGN_ID
)
private
Long
id
;
private
String
username
;
/** BCrypt; first boot is initialized by AdminInitializer. */
@JsonIgnore
private
String
passwordHash
;
private
String
displayName
;
private
String
email
;
/** SUPER_ADMIN / OPERATOR / FINANCE / READONLY */
private
String
role
;
/** 1=ACTIVE 0=DISABLED */
private
Integer
status
;
private
Integer
mustChangePassword
;
private
LocalDateTime
lastLoginAt
;
private
String
lastLoginIp
;
private
Long
createdBy
;
private
LocalDateTime
createdAt
;
private
LocalDateTime
updatedAt
;
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/Plan.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
entity
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
lombok.Data
;
import
java.math.BigDecimal
;
import
java.time.LocalDateTime
;
@Data
@TableName
(
"plans"
)
public
class
Plan
{
@TableId
(
type
=
IdType
.
ASSIGN_ID
)
private
Long
id
;
/** FREE / PRO / TEAM / ENTERPRISE */
private
String
code
;
private
String
name
;
private
BigDecimal
priceMonthly
;
private
Integer
monthlyCredits
;
/** 0=unlimited */
private
Integer
maxMembers
;
/** 0=unlimited */
private
Integer
maxProjects
;
private
Integer
maxConcurrentTasks
;
/** JSON */
private
String
features
;
/** 1=ACTIVE 0=INACTIVE */
private
Integer
isActive
;
private
LocalDateTime
updatedAt
;
private
Long
updatedBy
;
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/SystemSetting.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
entity
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
lombok.Data
;
import
java.time.LocalDateTime
;
@Data
@TableName
(
"system_settings"
)
public
class
SystemSetting
{
@TableId
(
type
=
IdType
.
ASSIGN_ID
)
private
Long
id
;
private
String
settingKey
;
private
String
settingValue
;
private
LocalDateTime
updatedAt
;
private
Long
updatedBy
;
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/TenantQuota.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
entity
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
lombok.Data
;
import
java.math.BigDecimal
;
import
java.time.LocalDateTime
;
@Data
@TableName
(
"tenant_quotas"
)
public
class
TenantQuota
{
@TableId
(
value
=
"tenant_id"
,
type
=
IdType
.
INPUT
)
private
Long
tenantId
;
private
Integer
maxMembers
;
private
Integer
maxProjects
;
private
Integer
maxMonthlyCredits
;
private
Integer
maxConcurrentTasks
;
private
BigDecimal
monthlyBudgetLimit
;
private
String
notes
;
private
LocalDateTime
updatedAt
;
private
Long
updatedBy
;
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/AdminAuditLogMapper.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
mapper
;
import
com.baomidou.mybatisplus.annotation.InterceptorIgnore
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.yaoai.domain.entity.AdminAuditLog
;
import
org.apache.ibatis.annotations.Mapper
;
@Mapper
@InterceptorIgnore
(
tenantLine
=
"true"
)
public
interface
AdminAuditLogMapper
extends
BaseMapper
<
AdminAuditLog
>
{
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/AdminUserMapper.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
mapper
;
import
com.baomidou.mybatisplus.annotation.InterceptorIgnore
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.yaoai.domain.entity.AdminUser
;
import
org.apache.ibatis.annotations.Mapper
;
@Mapper
@InterceptorIgnore
(
tenantLine
=
"true"
)
public
interface
AdminUserMapper
extends
BaseMapper
<
AdminUser
>
{
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/PlanMapper.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
mapper
;
import
com.baomidou.mybatisplus.annotation.InterceptorIgnore
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.yaoai.domain.entity.Plan
;
import
org.apache.ibatis.annotations.Mapper
;
@Mapper
@InterceptorIgnore
(
tenantLine
=
"true"
)
public
interface
PlanMapper
extends
BaseMapper
<
Plan
>
{
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/SystemSettingMapper.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
mapper
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.baomidou.mybatisplus.core.toolkit.Wrappers
;
import
com.yaoai.domain.entity.SystemSetting
;
import
org.apache.ibatis.annotations.Mapper
;
import
java.util.Optional
;
@Mapper
public
interface
SystemSettingMapper
extends
BaseMapper
<
SystemSetting
>
{
default
Optional
<
SystemSetting
>
findByKey
(
String
key
)
{
return
Optional
.
ofNullable
(
selectOne
(
Wrappers
.<
SystemSetting
>
lambdaQuery
()
.
eq
(
SystemSetting:
:
getSettingKey
,
key
)));
}
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/TenantQuotaMapper.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
domain
.
mapper
;
import
com.baomidou.mybatisplus.annotation.InterceptorIgnore
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.yaoai.domain.entity.TenantQuota
;
import
org.apache.ibatis.annotations.Mapper
;
@Mapper
@InterceptorIgnore
(
tenantLine
=
"true"
)
public
interface
TenantQuotaMapper
extends
BaseMapper
<
TenantQuota
>
{
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/dto/StructuredVideoSubmission.java
0 → 100644
View file @
b7185a90
package
com
.
yaoai
.
pipeline
.
dto
;
import
lombok.Builder
;
import
lombok.Data
;
import
java.util.List
;
import
java.util.Map
;
/**
* 结构化视频生成的 pipeline 内部 DTO。
* 由 controller 层从 yaoai-api 的 {@code StructuredVideoGenerateRequest} 映射而来,
* 避免 yaoai-pipeline 反向依赖 yaoai-api。
*/
@Data
@Builder
public
class
StructuredVideoSubmission
{
private
Long
userId
;
private
Long
episodeId
;
private
Long
storyboardId
;
/** @图1 角色图 TOS key(旧版单角色字段,保留兼容;当 characterImageKeys 非空时忽略) */
private
String
characterImageKey
;
/** V16:多角色出镜,按 @图1..@图N 顺序排列;非空时优先于 characterImageKey */
private
List
<
String
>
characterImageKeys
;
/** 场景图 TOS key(紧跟在最后一个角色图之后) */
private
String
sceneImageKey
;
/** @图3+ 道具图 TOS key 列表(可空) */
private
List
<
String
>
propImageKeys
;
/** 风格图 TOS key(可空,永远是最后一张) */
private
String
styleImageKey
;
/** 用户原始结构化输入(5 段或单段 freeform),用于落库存证 */
private
Map
<
String
,
String
>
userPromptRaw
;
/** 拆开的 5 段,传给 SeedancePromptBuilder(与 freeformPrompt 二选一) */
private
String
characterAction
;
private
String
sceneEvent
;
private
String
cameraMovement
;
private
String
lightingAtmosphere
;
private
String
videoStyle
;
/** V17:单段 freeform 提示词,非空时优先于上面 5 段 */
private
String
freeformPrompt
;
private
Integer
durationSeconds
;
private
String
ratio
;
/** Seedance 模型 ID,为空时由 SeedanceServiceImpl 兜底为 ArkProperties.videoModel */
private
String
model
;
private
Boolean
generateAudio
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment