diff --git a/apps/stats/src/views/Stats/Overview/Overview.tsx b/apps/stats/src/views/Stats/Overview/Overview.tsx index cda2153..48ac177 100644 --- a/apps/stats/src/views/Stats/Overview/Overview.tsx +++ b/apps/stats/src/views/Stats/Overview/Overview.tsx @@ -77,8 +77,14 @@ const Overview: React.FC = () => { } }); - /* Get visitors - /* ---------------------------------------------------------------------- */ + /* 获取访客(visitors)数据 + ---------------------------------------------------------------------- + 说明: + - 该部分通过 `useTinybirdQuery` 向 Tinybird(或 stats 后端)发起查询,获取按日期聚合的访问量数据。 + - 接着使用 `sanitizeChartData` 对数据做日期范围内的聚合/缺失点填充/离群值检测(由 range 决定日/周/月聚合策略)。 + - 最终把数据映射为 `GhAreaChart` 可识别的格式(date, value, formattedValue, label),并传入展示组件。 + - 这里的 visitorsChartData 是概览页中显示“访客曲线”的数据源。 + */ const visitorsParams = { site_uuid: statsConfig?.id || '', date_from: formatQueryDate(startDate), @@ -94,7 +100,11 @@ const Overview: React.FC = () => { }); const visitorsChartData = useMemo(() => { - return sanitizeChartData(visitorsData as WebKpiDataItem[] || [], range, 'visits' as keyof WebKpiDataItem, 'sum')?.map((item: WebKpiDataItem) => { + // 先对原始数据进行清洗和聚合(按 range: day/week/month) + const sanitized = sanitizeChartData(visitorsData as WebKpiDataItem[] || [], range, 'visits' as keyof WebKpiDataItem, 'sum') || []; + + // 将每项转换为 GhAreaChart 所需的字段格式 + return sanitized.map((item: WebKpiDataItem) => { const value = Number(item.visits); const safeValue = isNaN(value) ? 0 : value; return { diff --git a/ghost/core/core/server/data/schema/commands.js b/ghost/core/core/server/data/schema/commands.js index e46d977..bc5191d 100644 --- a/ghost/core/core/server/data/schema/commands.js +++ b/ghost/core/core/server/data/schema/commands.js @@ -405,7 +405,7 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, constraintN * @param {Object} configuration - contains all configuration for this function * @param {string} configuration.fromTable - name of the table to add the foreign key to * @param {string} configuration.fromColumn - column of the table to add the foreign key to - * @param {string} configuration.toTable - name of the table to point the foreign key to + * @param {string} configuration.toTable - name of the table to point the foreign key to * @param {string} configuration.toColumn - column of the table to point the foreign key to * @param {string} [configuration.constraintName] - name of the FK to delete * @param {import('knex').Knex} [configuration.transaction] - connection object containing knex reference diff --git a/ghost/core/core/server/models/user.js b/ghost/core/core/server/models/user.js index daeecd9..794295e 100644 --- a/ghost/core/core/server/models/user.js +++ b/ghost/core/core/server/models/user.js @@ -77,9 +77,9 @@ User = ghostBookshelf.Model.extend({ format(options) { if (options.website && - !validator.isURL(options.website, { - require_protocol: true, - protocols: ['http', 'https'] + validator.isURL(options.website, { + require_protocol: true, // 必须包含协议 + protocols: ['http', 'https'] // 只允许HTTP和HTTPS协议 })) { options.website = 'http://' + options.website; } @@ -96,8 +96,9 @@ User = ghostBookshelf.Model.extend({ }, parse() { + //将数据库存储的URL转换为绝对URL const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments); - + //将转换后的URL存储回属性中 ['profile_image', 'cover_image'].forEach((attr) => { if (attrs[attr]) { attrs[attr] = urlUtils.transformReadyToAbsolute(attrs[attr]); @@ -121,6 +122,7 @@ User = ghostBookshelf.Model.extend({ onDestroying(model, options) { ghostBookshelf.Model.prototype.onDestroying.apply(this, arguments); + //清除用户角色关联表中的相关记录 return (options.transacting || ghostBookshelf.knex)('roles_users') .where('user_id', model.id) .del(); @@ -185,7 +187,7 @@ User = ghostBookshelf.Model.extend({ const tasks = []; let passwordValidation = {}; - ghostBookshelf.Model.prototype.onSaving.apply(this, arguments); + ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);// 调用父类的 onSaving 方法 /** * Bookshelf call order: @@ -194,7 +196,7 @@ User = ghostBookshelf.Model.extend({ * * Before we can generate a slug, we have to ensure that the name is not blank. */ - if (!this.get('name')) { + if (!this.get('name')) {// 如果 name 为空,就抛出异常 throw new errors.ValidationError({ message: tpl(messages.valueCannotBeBlank, { tableName: this.tableName, @@ -205,7 +207,7 @@ User = ghostBookshelf.Model.extend({ // If the user's email is set & has changed & we are not importing if (self.hasChanged('email') && self.get('email') && !options.importing) { - tasks.push((function lookUpGravatar() { + tasks.push((function lookUpGravatar() {// 如果 email 发生了改变,就去 Gravatar 上查找对应的头像 const {gravatar} = require('../lib/image'); return gravatar.lookup({ @@ -218,7 +220,7 @@ User = ghostBookshelf.Model.extend({ })()); } - if (this.hasChanged('slug') || !this.get('slug')) { + if (this.hasChanged('slug') || !this.get('slug')) {// 如果 slug 发生了改变,或者 slug 为空,就生成一个新的 slug tasks.push((function generateSlug() { return ghostBookshelf.Model.generateSlug( User, @@ -256,7 +258,7 @@ User = ghostBookshelf.Model.extend({ return; } - if (options.importing) { + if (options.importing) {// 如果是导入数据,就不进行密码验证 // always set password to a random uid when importing this.set('password', security.identifier.uid(50)); @@ -264,7 +266,7 @@ User = ghostBookshelf.Model.extend({ if (this.get('status') !== 'inactive') { this.set('status', 'locked'); } - } else { + } else {// 如果不是导入数据,就进行密码验证 // CASE: we're not importing data, validate the data passwordValidation = validatePassword(this.get('password'), this.get('email')); @@ -274,7 +276,7 @@ User = ghostBookshelf.Model.extend({ })); } } - + //密码哈希处理 tasks.push((function hashPassword() { return security.password.hash(self.get('password')) .then(function (hash) { @@ -535,7 +537,7 @@ User = ghostBookshelf.Model.extend({ const options = this.filterOptions(unfilteredOptions, 'edit'); const self = this; const ops = []; - + // 检查用户是否只有一个角色 if (data.roles && data.roles.length > 1) { return Promise.reject( new errors.ValidationError({ @@ -547,6 +549,7 @@ User = ghostBookshelf.Model.extend({ if (data.email) { ops.push(function checkForDuplicateEmail() { return self.getByEmail(data.email, options).then(function then(user) { + // 如果查询到有用户使用了相同的邮箱,且该用户不是当前用户,就返回错误 if (user && user.id !== options.id) { return Promise.reject(new errors.ValidationError({ message: tpl(messages.userUpdateError.emailIsAlreadyInUse) @@ -559,7 +562,7 @@ User = ghostBookshelf.Model.extend({ ops.push(function update() { return ghostBookshelf.Model.edit.call(self, data, options).then(async (user) => { let roleId; - + //如果没有角色信息就返回用户 if (!data.roles || !data.roles.length) { return user; } @@ -571,7 +574,7 @@ User = ghostBookshelf.Model.extend({ if (roles.models[0].id === roleId) { return; } - + //用户验证逻辑 if (ASSIGNABLE_ROLES.includes(roleId)) { // return if the role is already assigned if (roles.models[0].get('name') === roleId) { @@ -595,6 +598,7 @@ User = ghostBookshelf.Model.extend({ ); } }).then((roleToAssign) => { + // Owner被禁止分配 if (roleToAssign && roleToAssign.get('name') === 'Owner') { return Promise.reject( new errors.ValidationError({ @@ -602,14 +606,14 @@ User = ghostBookshelf.Model.extend({ }) ); } else if (roleToAssign) { - // assign all other roles + // assign all other roles 更新角色关联 return user.roles().updatePivot({role_id: roleToAssign.id}); } }).then(() => { options.status = 'all'; - return self.findOne({id: user.id}, options); + return self.findOne({id: user.id}, options);//重新获取完整用户信息 }).then((model) => { - model._changed = user._changed; + model._changed = user._changed;//保留变更标记 return model; }); });