# CreatorDB Backend Engineer Interview Test
- [Question](https://hackmd.io/@l8mSeLGERMCGy_KadfR22A/S1N45Q9Zee)
- [Reference answer](https://hackmd.io/@l8mSeLGERMCGy_KadfR22A/BJJfN8yGex)
---
# 1. Algorithm - Fill in Missing Daily Metrics
---
You are working on a social media analytics platform. The system provides an API that returns a creator’s recent metric data. The API returns at most 7 metric entries and at least 1 metric entry, sorted in ascending order by date. However, the entries might not cover 7 consecutive days due to missing data in the database.
Your task is to implement a function:
`fillMissingMetrics(data: Metric[])`: Metric[] that fills in the missing days so that the result contains exactly 7 days of data, from 6 days ago up to today.
---
## Input
- data: an array of up to 7 Metric objects, sorted by date (ascending):
```typescript=
type Metric = {
date: number; // UTC timestamp at midnight
averageLikesCount: number;
followersCount: number;
averageEngagementRate: number;
};
```
- The function should generate entries for the date range:
```
[today - 6d, ..., today]
```
- where "today" is the current date at midnight UTC.
```
(i.e., new Date().setUTCHours(0, 0, 0, 0))
```
---
## Fill-in Logic
---
For each missing day:
- Look for the nearest available date (this can be from either the past or future).
- If two dates are equally distant, prefer the older date (i.e., the earlier date).
- Fill the missing days by using the nearest date according to the rules above.
- Return a list of exactly 7 metrics, one for each day in the target date range, sorted in ascending order.
---
## Example Input
---
```typescript=
const example1 = [
{
date: 1738368000000, // -11d
averageLikesCount: 100,
followersCount: 200,
averageEngagementRate: 0.01
},
{
date: 1738540800000, // -9d
averageLikesCount: 105,
followersCount: 202,
averageEngagementRate: 0.012
},
{
date: 1738713600000, // -7d
averageLikesCount: 110,
followersCount: 205,
averageEngagementRate: 0.015
},
{
date: 1738800000000, // -6d
averageLikesCount: 120,
followersCount: 208,
averageEngagementRate: 0.02
},
{
date: 1739068800000, // -3d
averageLikesCount: 130,
followersCount: 210,
averageEngagementRate: 0.022
},
{
date: 1739155200000, // -2d
averageLikesCount: 140,
followersCount: 215,
averageEngagementRate: 0.023
},
{
date: 1739328000000, // 0d
averageLikesCount: 150,
followersCount: 220,
averageEngagementRate: 0.025
},
];
// Today is represented by 1739328000000 in this example
```
```typescript=
const example2 = [
{
date: 1738886400000, // -5d
averageLikesCount: 120,
followersCount: 208,
averageEngagementRate: 0.02
},
{
date: 1739328000000, // 0d
averageLikesCount: 150,
followersCount: 220,
averageEngagementRate: 0.025
},
];
// Today is represented by 1739328000000 in this example
```
---
## 🎯 Expected Output
---
- Example Output from example1:
```typescript=
[
{ date: ..., data from -6d }, // -6d → original
{ date: ..., data from -6d }, // -5d → fallback to -6d
{ date: ..., data from -3d }, // -4d → fallback to -3d
{ date: ..., data from -3d }, // -3d → original
{ date: ..., data from -2d }, // -2d → original
{ date: ..., data from -2d }, // -1d → fallback to -2d
{ date: ..., data from 0d } // 0d → original
]
```
- Example Output from example2:
```typescript=
[
{ date: ..., data from -5d }, // -6d → fallback forward to -5d
{ date: ..., data from -5d }, // -5d → original
{ date: ..., data from -5d }, // -4d → fallback to -5d
{ date: ..., data from -5d }, // -3d → fallback to -5d
{ date: ..., data from -5d }, // -2d → fallback to -5d (equal distance to 0d, prefer older)
{ date: ..., data from 0d }, // -1d → fallback to 0d
{ date: ..., data from 0d }, // 0d → original
]
```
---
## 📝 Function Skeleton
```typescript=
/**
Fills in missing metrics to ensure complete data for the last 7 days
@param data - Array of metric objects sorted by date in ascending order
@returns Array of metrics with exactly 7 days of data (from 6 days ago to today)
*/
function fillMissingMetrics(data: Metric[]): Metric[] {
// Your implementation here
}
```
---
## Solution
- 撇開資料結構,就是找最小值。
- 時間複雜度,`Q(7*n)`
-
```cpp=
struct Metric {
long long date; // UTC timestamp at midnight
int averageLikesCount;
int followersCount;
double averageEngagementRate;
};
long long getTodayMidnightUTC() {
time_t now = time(nullptr);
tm* utc = gmtime(&now);
utc->tm_hour = 0;
utc->tm_min = 0;
utc->tm_sec = 0;
return static_cast<long long>(mktime(utc)) * 1000;
}
Metric findClosedDay(long long date, Metric[] & data) {
// Your implementation here
Metric closest = data[0];
long long minDistance = abs(data[0].date - targetDate);
for (const auto& metric : data) {
long long distance = abs(metric.date - targetDate);
if (distance < minDistance ||
(distance == minDistance && metric.date < closest.date)
) {
closest = metric;
minDistance = distance;
}
}
Metric result = closest;
result.date = targetDate;
return result;
}
vector<Metric> fillMissingMetrics(const vector<Metric>& data) {
const long long MS_PER_DAY = 86400000LL;
long long today = getTodayMidnightUTC();
vector<Metric> ret;
// Your implementation here
for (int i = 6; i >= 0; --i) {
long long date = today - i * MS_PER_DAY;
Metric day = findClosedDay(date, data);
ret.push_back(day);
}
return ret;
}
```
---
# 2. Debug
---
Help me to debug this code, and explan why
```typescript=
async function getPage(url) {
var response = await fetch(url);
var data = await response.text();
return data;
}
async function getYoutubeData(youtubeIds) {
var promises = [];
for (var i = 0; i < youtubeIds.length; i++) {
var promise = new Promise(async (resolve, reject) => {
try {
var channelURL = https://www.youtube.com/${youtubeIds[i]};
var channelPage = await getPage(channelURL);
var videosURL = `https://www.youtube.com/${youtubeIds[i]}/videos`;
var videosPage = await getPage(videosURL);
resolve({ channelPage, videosPage });
} catch (e) {
reject(e);
}
});
promises.push(promise);
}
var results = await Promise.all(promises);
return results;
}
var youtubeIds = ['@darbbq', '@oojimateru', '@homemeat_clip'];
getYoutubeData(youtubeIds);
```
---
## Solution
---
- Bug
- 字串插值語法錯誤
```typescript=
const channelURL = `https://www.youtube.com/${youtubeId}`;
```
- Promise 使用不當:在循環中不必要地建立新 Promise
- 串行請求效率低:依序請求每個頻道,沒有充分利用並行處理
- 一個頻道失敗會導致整個請求失敗
- 改進說明
```typescript=
async function getPage(url) {
const response = await fetch(url);
const data = await response.text();
return data;
}
async function getChannelData(youtubeId) {
try {
const channelURL = `https://www.youtube.com/${youtubeId}`;
const videosURL = `https://www.youtube.com/${youtubeId}/videos`;
const [channelPage, videosPage] = await Promise.all([
getPage(channelURL),
getPage(videosURL)
]);
return { channelPage, videosPage };
} catch (e) {
console.error(`获取 ${youtubeId} 数据失败:`, e);
return null; // 或者可以根据需要处理错误
}
}
async function getYoutubeData(youtubeIds: string[]): Promise<ChannelData[]> {
const promises = [];
// 使用 for 循环创建 promise 数组
for (let i = 0; i < youtubeIds.length; i++) {
promises.push(getChannelData(youtubeIds[i]));
}
// 等待所有 promise 完成
const allResults = await Promise.all(promises);
// 过滤 null 值
const finalResults = [];
for (const result of allResults) {
if (result) {
finalResults.push(result);
}
}
return finalResults;
}
const youtubeIds = ['@darbbq', '@oojimateru', '@homemeat_clip'];
getYoutubeData(youtubeIds).then(console.log).catch(console.error);
```
---
# 3. Database Schema Design
---
- Scenario:
You need to design a database schema for a headless API service with **quota counting**. The system will track API usage, manage user credits, and provide usage analytics.
- Requirements
- Credit Management: Track prepurchased credits and deduct them based on API endpoint usage
- Usage Tracking: Record detailed API call information for auditing and troubleshooting
- Historical Record Keeping: Store monthly usage data for analytics and reporting
- Monthly Analysis: Enable users to view their API usage aggregated by month
- Endpoint-Specific Quotas: Different API endpoints have different credit costs
```typescript=
const enum APIEndpoint {
// creator
SubmitCreators = '/submit-creators',
DiscoverCreators = '/discover-creators',
GetCreatorInfo = '/get-creator-info',
// keyword
GetTopicItems = '/get-topic-items',
GetNicheItems = '/get-niche-items',
GetHashtagItems = '/get-hashtag-items',
// Add other endpoints as needed
}
const apiQuotaMap: Record<APIEndpoint, number> = {
// creator
[APIEndpoint.SubmitCreators]: 1,
[APIEndpoint.DiscoverCreators]: 2,
[APIEndpoint.GetCreatorInfo]: 3,
// keyword
[APIEndpoint.GetTopicItems]: 1,
[APIEndpoint.GetNicheItems]: 1,
[APIEndpoint.GetHashtagItems]: 1,
// Add other mappings as needed
} as const
```
---
### Environment Constraints:
---
- Database: Choose either NoSQL (MongoDB/Firestore) OR SQL (MySQL/PostgreSQL) based on your assessment
- Integration: Service is already in use and will be extended with this new feature (maintain backward compatibility)
- Performance: Design must be scalable and extendable to handle growing usage
- Efficiency: Schema should minimize processing time and operational costs
- Documentation: Schema should be readable and easily explainable to non-technical stakeholders
---
### Task:
---
Complete the `UserTableScheme` and any other necessary types by designing appropriate database schema structures to fulfill the requirements above. You need to implement the `apiUsageHistory` field and any additional tables or structures needed.
```typescript=
// design database storage scheme
type UserTableScheme =
{
userId: string;
prepurchasedCredit: number;
// implement here
apiUsageHistory: ???
...
}
```
---
### Solution:
---
```typescript=
// API端点类型(保持不变)
const enum APIEndpoint {
// creator
SubmitCreators = '/submit-creators',
DiscoverCreators = '/discover-creators',
GetCreatorInfo = '/get-creator-info',
// keyword
GetTopicItems = '/get-topic-items',
GetNicheItems = '/get-niche-items',
GetHashtagItems = '/get-hashtag-items',
}
// 配额映射(保持不变)
const apiQuotaMap: Record<APIEndpoint, number> = {
[APIEndpoint.SubmitCreators]: 1,
[APIEndpoint.DiscoverCreators]: 2,
[APIEndpoint.GetCreatorInfo]: 3,
[APIEndpoint.GetTopicItems]: 1,
[APIEndpoint.GetNicheItems]: 1,
[APIEndpoint.GetHashtagItems]: 1,
} as const;
// 主用户表
type UserTableScheme = {
userId: string; // 用户唯一ID
email: string; // 用户邮箱
prepurchasedCredit: number; // 预购信用点数
currentMonthCreditUsed: number; // 当月已用信用
createdAt: Date; // 账户创建时间
updatedAt: Date; // 最后更新时间
};
// API使用记录表
type ApiUsageRecord = {
recordId: string; // 记录唯一ID
userId: string; // 关联用户ID
endpoint: APIEndpoint; // 调用的API端点
creditsUsed: number; // 消耗的信用点数
timestamp: Date; // 调用时间
status: 'success' | 'failed'; // 调用状态
responseSize?: number; // 响应数据大小(可选)
ipAddress?: string; // 调用IP(可选)
};
// 月度汇总表
type MonthlyUsageSummary = {
summaryId: string; // 汇总ID
userId: string; // 关联用户ID
year: number; // 年份
month: number; // 月份 (1-12)
totalCreditsUsed: number; // 该月总使用量
endpointBreakdown: { // 按端点分类的使用量
endpoint: APIEndpoint;
count: number;
creditsUsed: number;
}[];
};
// 信用变更记录表(可选)
type CreditTransaction = {
transactionId: string; // 交易ID
userId: string; // 关联用户ID
amount: number; // 变更数量(正数为增加)
type: 'purchase' | 'usage' | 'manual_adjustment' | 'refund';
referenceId?: string; // 关联ID(如订单ID)
timestamp: Date; // 变更时间
remainingCredit: number; // 变更后剩余信用
};
```
1. 信用扣除流程
Credit Management: Track prepurchased credits and deduct them based on API endpoint usage
```typescript=
async function deductCredits(userId: string, endpoint: APIEndpoint) {
// 开始事务
const creditsNeeded = apiQuotaMap[endpoint];
// 1. 检查用户信用
const user = await db.user.findByPk(userId);
if (user.prepurchasedCredit < creditsNeeded) {
throw new Error('信用不足');
}
// 2. 扣除信用(原子操作)
await db.user.update({
prepurchasedCredit: user.prepurchasedCredit - creditsNeeded,
currentMonthCreditUsed: user.currentMonthCreditUsed + creditsNeeded
}, { where: { userId } });
// 3. 记录使用日志
await db.usageRecords.create({
userId,
endpoint,
creditsUsed: creditsNeeded,
timestamp: new Date(),
status: 'success'
});
// 4. 更新月度汇总(异步)
updateMonthlySummary(userId, endpoint, creditsNeeded);
}
```
```sql=
BEGIN;
-- 1. 扣除信用(含檢查條件)
UPDATE "user"
SET
prepurchased_credit = prepurchased_credit - :creditsNeeded,
current_month_credit_used = current_month_credit_used + :creditsNeeded
WHERE user_id = :userId
AND prepurchased_credit >= :creditsNeeded;
-- 若沒有成功扣除(ROW_COUNT = 0),應在應用層 ROLLBACK 並報錯
-- 2. 新增使用紀錄
INSERT INTO api_usage_record (
record_id,
user_id,
endpoint,
credits_used,
timestamp,
status
) VALUES (
gen_random_uuid(), -- 或使用 uuid_generate_v4(),需安裝 pgcrypto 或 uuid-ossp
:userId,
:endpoint,
:creditsNeeded,
:timestamp,
'success'
);
-- 3. 更新或插入月度彙總(upsert)
INSERT INTO monthly_usage_summary (
user_id,
year,
month,
endpoint,
count,
credits_used
) VALUES (
:userId,
EXTRACT(YEAR FROM :timestamp)::int,
EXTRACT(MONTH FROM :timestamp)::int,
:endpoint,
1,
:creditsNeeded
)
ON CONFLICT (user_id, year, month, endpoint)
DO UPDATE SET
count = monthly_usage_summary.count + 1,
credits_used = monthly_usage_summary.credits_used + EXCLUDED.credits_used;
COMMIT;
```
2. 每月報表生成
Monthly Analysis: Enable users to view their API usage aggregated by month
```typescript=
async function generateMonthlyReport(userId: string, year: number, month: number) {
// 从预计算的月度汇总表获取
const report = await db.monthlySummaries.findOne({
where: { userId, year, month }
});
// 或实时聚合(性能较低但更准确)
const realtimeReport = await db.usageRecords.findAll({
where: {
userId,
timestamp: {
[Op.between]: [
new Date(year, month - 1, 1),
new Date(year, month, 0)
]
}
},
group: ['endpoint'],
attributes: [
'endpoint',
[db.fn('COUNT', 'id'), 'count'],
[db.fn('SUM', 'creditsUsed'), 'creditsUsed']
]
});
return report || realtimeReport;
}
```
- 針對某api 用量 給出 月報
```sql=
SELECT
endpoint,
COUNT(record_id) AS count,
SUM(credits_used) AS creditsUsed
FROM
usage_records
WHERE
user_id = '使用者ID' AND
timestamp >= '2025-05-01' AND
timestamp < '2025-06-01'
GROUP BY
endpoint;
```