把活层退役旧品牌「造梦AI」统一改为现行「绘境AI」,brand-invariant 门红线归零。构成: ~412 Java @author 署名 + game-studio/index.html 浏览器标题 + game-runtime/package.json 描述 + game-admin/.env VITE_APP_TITLE 运营后台标题 + contracts API title/@Schema 用户协议示例/events 描述 + docs-design mockup 品牌位 + 各模块注释。全是注释/显示/元数据,零逻辑改动。 zaomeng 拼音持久化标识符按 2026-06-24 评审 GR-05 不动(347 处保留)。 白名单(docs/ip 法律备案 / _archive / 带日期留痕)与 AGENTS.md 门定义自身不动。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
217 lines
7.0 KiB
TypeScript
217 lines
7.0 KiB
TypeScript
import request from '@/config/axios'
|
||
import { isMock, mockPage, mockResult } from './mock'
|
||
|
||
/**
|
||
* 绘境AI —— ad 模块 admin 接口(广告位管理 + 广告收入台账对账)
|
||
* 契约:contracts/api-schemas/ad.yaml(字段严格对齐契约#7 ad-slot.schema.json,不重造广告位结构)
|
||
* - POST /admin-api/ad/slot/create 新建广告位(slotId 业务唯一)
|
||
* - PUT /admin-api/ad/slot/update 更新(按主键 id,slotId 不可改)
|
||
* - DELETE /admin-api/ad/slot/delete?id= 逻辑删除
|
||
* - GET /admin-api/ad/slot/get?id= 详情
|
||
* - GET /admin-api/ad/slot/page 分页(按 type/provider/enabled 筛选)
|
||
* - GET /admin-api/ad/revenue/page 广告收入台账分页(归因/计费对账,只读)
|
||
* 端与鉴权:admin-api 走 huijing 管理员 Token + RBAC;CommonResult code=0 成功(request 已解包 data)。
|
||
* 资金口径:金额一律「分」(BIGINT),禁浮点;ecpmFloor/ecpm/revenueAmount 均为分。
|
||
*/
|
||
|
||
/** 合规植入规则(对齐 ad.yaml AdComplianceVO / 契约#7 compliance) */
|
||
export interface AdComplianceVO {
|
||
blockMinor?: boolean // 识别为未成年则不展示/不计费
|
||
maxPerSession?: number // 单会话最大展示次数
|
||
}
|
||
|
||
/** 广告位新建/更新入参(对齐 ad.yaml AdSlotSaveReqVO) */
|
||
export interface AdSlotSaveReqVO {
|
||
id?: number // 更新必填、新建不传
|
||
slotId: string // 业务唯一键 uk_slot(新建唯一、更新不可改)
|
||
type: string // rewarded | interstitial | banner
|
||
provider: string // csj | gdt | mock(MVP 默认 mock)
|
||
placement?: string // game_end | pause | feed
|
||
providerSlotId?: string // 联盟侧真实广告位 ID(provider!=mock 时必填)
|
||
ecpmFloor?: number // eCPM 底价(分)
|
||
enabled: boolean
|
||
compliance?: AdComplianceVO
|
||
}
|
||
|
||
/** 广告位详情(对齐 ad.yaml AdSlotRespVO) */
|
||
export interface AdSlotRespVO {
|
||
id: number
|
||
slotId: string
|
||
type: string
|
||
provider: string
|
||
placement?: string
|
||
providerSlotId?: string
|
||
ecpmFloor?: number
|
||
enabled: boolean
|
||
compliance?: AdComplianceVO
|
||
createTime?: string
|
||
updateTime?: string
|
||
}
|
||
|
||
/** 广告位分页查询参数(对齐 ad.yaml /admin-api/ad/slot/page) */
|
||
export interface AdSlotPageReqVO extends PageParam {
|
||
type?: string
|
||
provider?: string
|
||
enabled?: boolean
|
||
}
|
||
|
||
/** 广告收入台账记录(对齐 ad.yaml AdRevenueRespVO) */
|
||
export interface AdRevenueRespVO {
|
||
id: number // = trade 侧 source_ref 对账锚点
|
||
slotId: string
|
||
gameId: number
|
||
creatorUserId: number // 收入归因到的创作者用户 ID
|
||
eventType: string // impression | reward
|
||
provider: string
|
||
ecpm?: number // 分
|
||
revenueAmount: number // 本条广告收入(分)
|
||
traceId: string // 计费幂等键 / 对账 traceId
|
||
settleStatus: number // 0未结算 1已结算
|
||
statDate?: string // 归集日 yyyy-MM-dd
|
||
createTime?: string
|
||
}
|
||
|
||
/** 广告收入台账查询参数(对齐 ad.yaml /admin-api/ad/revenue/page) */
|
||
export interface AdRevenuePageReqVO extends PageParam {
|
||
slotId?: string
|
||
gameId?: number
|
||
creatorUserId?: number
|
||
eventType?: string // impression | reward
|
||
settleStatus?: number // 0未结算 1已结算
|
||
statDate?: string
|
||
}
|
||
|
||
// ---- mock 数据 ----
|
||
const mockSlotList: AdSlotRespVO[] = [
|
||
{
|
||
id: 1,
|
||
slotId: 'slot_game_end_rewarded',
|
||
type: 'rewarded',
|
||
provider: 'mock',
|
||
placement: 'game_end',
|
||
providerSlotId: '',
|
||
ecpmFloor: 3500,
|
||
enabled: true,
|
||
compliance: { blockMinor: true, maxPerSession: 3 },
|
||
createTime: '2026-06-05 10:00:00',
|
||
updateTime: '2026-06-05 10:00:00'
|
||
},
|
||
{
|
||
id: 2,
|
||
slotId: 'slot_pause_interstitial',
|
||
type: 'interstitial',
|
||
provider: 'mock',
|
||
placement: 'pause',
|
||
providerSlotId: '',
|
||
ecpmFloor: 2000,
|
||
enabled: true,
|
||
compliance: { blockMinor: true, maxPerSession: 5 },
|
||
createTime: '2026-06-05 10:05:00',
|
||
updateTime: '2026-06-05 10:05:00'
|
||
},
|
||
{
|
||
id: 3,
|
||
slotId: 'slot_feed_banner',
|
||
type: 'banner',
|
||
provider: 'mock',
|
||
placement: 'feed',
|
||
providerSlotId: '',
|
||
ecpmFloor: 800,
|
||
enabled: false,
|
||
compliance: { blockMinor: true, maxPerSession: 10 },
|
||
createTime: '2026-06-05 10:10:00',
|
||
updateTime: '2026-06-06 09:00:00'
|
||
}
|
||
]
|
||
|
||
const mockRevenueList: AdRevenueRespVO[] = [
|
||
{
|
||
id: 90001,
|
||
slotId: 'slot_game_end_rewarded',
|
||
gameId: 2001,
|
||
creatorUserId: 30001,
|
||
eventType: 'reward',
|
||
provider: 'mock',
|
||
ecpm: 3500,
|
||
revenueAmount: 3,
|
||
traceId: 'trace-aaa-0001',
|
||
settleStatus: 0,
|
||
statDate: '2026-06-07',
|
||
createTime: '2026-06-07 12:30:00'
|
||
},
|
||
{
|
||
id: 90002,
|
||
slotId: 'slot_pause_interstitial',
|
||
gameId: 2002,
|
||
creatorUserId: 30002,
|
||
eventType: 'impression',
|
||
provider: 'mock',
|
||
ecpm: 2000,
|
||
revenueAmount: 2,
|
||
traceId: 'trace-bbb-0002',
|
||
settleStatus: 1,
|
||
statDate: '2026-06-06',
|
||
createTime: '2026-06-06 18:10:00'
|
||
}
|
||
]
|
||
|
||
/** 广告位分页 */
|
||
export const getAdSlotPage = async (params: AdSlotPageReqVO) => {
|
||
if (isMock()) {
|
||
let filtered = mockSlotList
|
||
if (params.type) filtered = filtered.filter((it) => it.type === params.type)
|
||
if (params.provider) filtered = filtered.filter((it) => it.provider === params.provider)
|
||
if (params.enabled != null) filtered = filtered.filter((it) => it.enabled === params.enabled)
|
||
return mockPage(filtered, params.pageNo, params.pageSize)
|
||
}
|
||
return await request.get({ url: '/ad/slot/page', params })
|
||
}
|
||
|
||
/** 广告位详情 */
|
||
export const getAdSlot = async (id: number) => {
|
||
if (isMock()) {
|
||
return mockResult(mockSlotList.find((it) => it.id === id) || mockSlotList[0])
|
||
}
|
||
return await request.get({ url: '/ad/slot/get?id=' + id })
|
||
}
|
||
|
||
/** 新建广告位(返回主键 ID) */
|
||
export const createAdSlot = async (data: AdSlotSaveReqVO) => {
|
||
if (isMock()) {
|
||
return mockResult(Date.now())
|
||
}
|
||
return await request.post({ url: '/ad/slot/create', data })
|
||
}
|
||
|
||
/** 更新广告位(slotId 不可改) */
|
||
export const updateAdSlot = async (data: AdSlotSaveReqVO) => {
|
||
if (isMock()) {
|
||
return mockResult(true)
|
||
}
|
||
return await request.put({ url: '/ad/slot/update', data })
|
||
}
|
||
|
||
/** 删除广告位(逻辑删除) */
|
||
export const deleteAdSlot = async (id: number) => {
|
||
if (isMock()) {
|
||
return mockResult(true)
|
||
}
|
||
return await request.delete({ url: '/ad/slot/delete?id=' + id })
|
||
}
|
||
|
||
/** 广告收入台账分页(只读对账) */
|
||
export const getAdRevenuePage = async (params: AdRevenuePageReqVO) => {
|
||
if (isMock()) {
|
||
let filtered = mockRevenueList
|
||
if (params.slotId) filtered = filtered.filter((it) => it.slotId === params.slotId)
|
||
if (params.gameId != null) filtered = filtered.filter((it) => it.gameId === params.gameId)
|
||
if (params.creatorUserId != null)
|
||
filtered = filtered.filter((it) => it.creatorUserId === params.creatorUserId)
|
||
if (params.eventType) filtered = filtered.filter((it) => it.eventType === params.eventType)
|
||
if (params.settleStatus != null)
|
||
filtered = filtered.filter((it) => it.settleStatus === params.settleStatus)
|
||
return mockPage(filtered, params.pageNo, params.pageSize)
|
||
}
|
||
return await request.get({ url: '/ad/revenue/page', params })
|
||
}
|