wenmo8 发布的文章

本篇介绍Manim中的淡入和淡出动画效果。

淡入
FadeIn
主要用于让对象以渐变的方式在场景中显现。

它的特点是视觉上柔和过渡,能自然地引导观众注意新出现的元素。

淡出
FadeOut
则是使对象逐渐透明直至消失。

用于移除已完成展示作用的元素或者进行场景切换过渡,它渐变消失的特点避免了元素突兀地离开场景。

这两个类为创建流畅的动画展示提供了有效的工具。

1. 动画函数概要

1.1. FadeIn

FadeIn
通过逐渐增加对象的透明度从完全透明(不可见)到完全不透明(正常显示),

给人一种平滑过渡的感觉,不会像突然出现的元素那样突兀。

FadeIn
主要参数有:

参数名称 类型 说明
mobjects Mobject 要进行淡入操作的对象
shift 向量类型 定义对象在淡入过程中的平移向量
target_position 位置或Mobject类型 确定对象淡入时的起始位置
scale 数值类型 指定对象在淡入初始时的缩放比例

shift
参数可以使对象在淡入的同时按照给定的方向和距离进行移动,增加动画的动态效果。

target_position
参数如果是一个
Mobject
,则以该对象的中心位置作为起始位置。

scale
参数使得对象会先按照此比例进行缩放,然后在淡入过程中恢复到原始大小。

FadeIn
的方法有:

名称 说明
clean_up_from_scene 在动画完成后清理场景
create_target 用于创建动画的目标状态

1.2. FadeOut

当某个元素在演示过程中不再需要展示,需要以一种自然的方式从场景中移除时,
FadeOut
就非常有用。


FadeIn
相反,它使对象的透明度从完全不透明逐渐变为完全透明,从而实现逐渐消失的视觉效果。

FadeOut
的主要参数有:

参数名称 类型 说明
mobjects Mobject 要进行淡出操作的对象
shift 向量类型 定义对象在淡出过程中的平移向量
target_position 位置或Mobject类型 确定对象在淡出时移动到的目标位置
scale 数值类型 用于指定对象在淡出过程中的缩放比例

shift
参数可以使对象在淡出的同时按照给定的方向和距离进行移动,增加动画的动态效果。

target_position
参数如果是一个
Mobject
,则会将其中心位置作为目标位置。

scale
参数使对象在逐渐消失的同时进行缩小或放大,为动画添加更多变化。

FadeOut
的方法和
FadeIn
的类似,不再赘述。

2. 使用示例

FadeIn

FadeOut
的参数不仅名称一样,含义也类似,只是一个用于淡入,一个用于淡出。

下面通过示例中演示淡入
FadeIn
的参数,对于
FadeOut
也是一样使用,反之亦然。

2.1. 基本使用

这个示例展示了
FadeIn

FadeOut
的基本使用,没有使用特殊参数,仅演示了对象的淡入和淡出效果。

# 创建一个圆形
c = Circle()
# 创建一个正方形
s = Square(color=BLUE)

# 将圆形添加到场景中
self.add(c)
self.wait()

# 淡入正方形 淡出圆形
self.play(FadeIn(s), FadeOut(c))

先将圆形添加到场景中,然后同时
淡入
正方形并
淡出
圆形。

2.2. FadeIn的shift参数

此示例演示
FadeIn

shift
参数,通过设置
shift
参数,使4个不同的形状在淡入时从场景的不同方向移动到其最终位置,展示了对象在淡入过程中的平移效果。

c = Circle(
    radius=0.8,
    fill_opacity=1,
    fill_color=RED_B,
)
t = Triangle(
    color=GREEN,
    fill_opacity=1,
    fill_color=GREEN_B,
)
s = Square(
    side_length=2,
    color=BLUE,
    fill_opacity=1,
    fill_color=BLUE_B,
)
r = Rectangle(
    height=2,
    width=1,
    color=YELLOW,
    fill_opacity=1,
    fill_color=YELLOW_B,
)
VGroup(c, t, s, r).arrange_in_grid(2, 2)

self.play(FadeIn(c, shift=UP * 2))
self.play(FadeIn(t, shift=DOWN * 2))
self.play(FadeIn(s, shift=LEFT * 2))
self.play(FadeIn(r, shift=RIGHT * 2))

2.3. FadeIn的target_position 参数

这个示例演示了
FadeIn

target_position
参数,将文本的淡入起始位置设置为点的位置,体现了对象可以从指定位置开始淡入的特点。

# 创建一个点
d1 = Dot(color=BLUE).shift(UP * 2 + LEFT * 2)
d2 = Dot(color=YELLOW).shift(UP * 2 + RIGHT * 2)
# 创建文本
t1 = Text("Hello,", color=BLUE)
t2 = Text("Manim!", color=YELLOW)
VGroup(t1, t2).arrange(RIGHT, buff=1)

# 将点添加到场景中
self.add(d1, d2)
# 等待1秒
self.wait()

# 淡入文本
self.play(FadeIn(t1, target_position=d1))
self.play(FadeIn(t2, target_position=d2))

如果是
FadeOut
使用
target_position
参数的话,元素会消失在上面示例中
Dot
的位置。

2.4. FadeOut的scale 参数

这个示例演示了
FadeOut

scale
参数,分别展示了对象在淡出时缩小(scale=0.5)和放大(scale=1.5)的效果,

说明了该参数对淡出时对象缩放的控制作用。

s = Star(color=RED)
h = RegularPolygram(6, color=YELLOW)
vg = VGroup(s, h).arrange(RIGHT, buff=1)
self.play(Create(vg), run_time=run_time)
self.wait()

# 淡出五角星 scale=0.5
self.play(FadeOut(s, scale=0.5))
# 淡出六边形 scale=2
self.play(FadeOut(h, scale=2))

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
fade.py
),

下载地址:
完整代码
(访问密码: 6872)

在浙江绍兴的山姆超市外,“黄牛”现象引发了广泛关注。这些“黄牛”通过提供带入和结账服务,让未办理会员卡的消费者也能进入超市购物。这一行为不仅扰乱了市场秩序,也对山姆会员商店的会员管理系统提出了挑战。今天,我们就来探讨一下,如何用Java实现一个更为健壮的会员管理系统,有效防止“黄牛”现象的发生。

一、问题背景与需求分析

山姆会员商店的会员制度是其核心竞争力之一,会员需要通过会员卡才能进入超市购物。然而,“黄牛”利用系统漏洞,通过多次带人进入和结账,从中牟利。为了打击这种行为,我们需要对会员管理系统进行升级,使其具备以下功能:

  1. 会员身份验证
    :确保只有合法会员才能进入超市。
  2. 消费频率监控
    :对会员的消费频率进行监控,及时发现异常消费行为。
  3. 黑名单管理
    :将确认的“黄牛”会员加入黑名单,禁止其再次进入超市。

二、系统设计

为了实现上述功能,我们可以设计一个基于Java的会员管理系统。系统主要包括以下几个模块:

  1. 会员验证模块
    :负责验证会员身份。
  2. 消费监控模块
    :负责监控会员的消费频率。
  3. 黑名单管理模块
    :负责黑名单的添加、查询和删除操作。

三、系统实现

1. 会员验证模块

会员验证模块主要通过会员卡号和密码进行身份验证。我们可以使用Java中的
HashMap
来存储会员信息,其中键为会员卡号,值为会员密码和其他相关信息。

import java.util.HashMap;
import java.util.Map;

public class MemberValidator {
    private Map<String, String> members;

    public MemberValidator() {
        members = new HashMap<>();
        // 初始化会员信息,这里以硬编码为例,实际应用中应从数据库读取
        members.put("123456", "password123");
        members.put("654321", "password321");
    }

    public boolean validateMember(String cardNumber, String password) {
        return members.containsKey(cardNumber) && members.get(cardNumber).equals(password);
    }
}
2. 消费监控模块

消费监控模块主要通过记录会员的消费时间和次数,来监控会员的消费频率。我们可以使用
HashMap
来存储会员的消费记录,其中键为会员卡号,值为消费时间列表。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ConsumptionMonitor {
    private Map<String, List<Long>> consumptionRecords;
    private static final int THRESHOLD = 5; // 设定消费频率阈值,例如5次
    private static final long INTERVAL = 3600 * 1000; // 设定时间间隔,例如1小时

    public ConsumptionMonitor() {
        consumptionRecords = new HashMap<>();
    }

    public void recordConsumption(String cardNumber) {
        long currentTime = System.currentTimeMillis();
        consumptionRecords.putIfAbsent(cardNumber, new ArrayList<>());
        List<Long> record = consumptionRecords.get(cardNumber);

        // 清理过时记录
        record.removeIf(time -> currentTime - time > INTERVAL);

        // 记录当前消费时间
        record.add(currentTime);

        // 检查是否超过阈值
        if (record.size() > THRESHOLD) {
            System.out.println("Warning: Member " + cardNumber + " has exceeded the consumption threshold!");
            // 可以将此处逻辑替换为将会员加入黑名单等操作
        }
    }
}
3. 黑名单管理模块

黑名单管理模块主要负责黑名单的添加、查询和删除操作。我们可以使用
HashSet
来存储黑名单中的会员卡号。

import java.util.HashSet;
import java.util.Set;

public class BlacklistManager {
    private Set<String> blacklist;

    public BlacklistManager() {
        blacklist = new HashSet<>();
    }

    public void addToBlacklist(String cardNumber) {
        blacklist.add(cardNumber);
    }

    public boolean isInBlacklist(String cardNumber) {
        return blacklist.contains(cardNumber);
    }

    public void removeFromBlacklist(String cardNumber) {
        blacklist.remove(cardNumber);
    }
}
4. 系统整合与测试

最后,我们将上述模块整合到一个系统中,并进行测试。

public class MemberManagementSystem {
    private MemberValidator validator;
    private ConsumptionMonitor monitor;
    private BlacklistManager blacklistManager;

    public MemberManagementSystem() {
        validator = new MemberValidator();
        monitor = new ConsumptionMonitor();
        blacklistManager = new BlacklistManager();
    }

    public boolean checkMemberEntry(String cardNumber, String password) {
        if (blacklistManager.isInBlacklist(cardNumber)) {
            System.out.println("Member " + cardNumber + " is in the blacklist, access denied!");
            return false;
        }

        if (validator.validateMember(cardNumber, password)) {
            // 记录消费
            monitor.recordConsumption(cardNumber);
            return true;
        } else {
            System.out.println("Invalid member credentials, access denied!");
            return false;
        }
    }

    public static void main(String[] args) {
        MemberManagementSystem system = new MemberManagementSystem();

        // 模拟会员进入超市
        String cardNumber = "123456";
        String password = "password123";

        for (int i = 0; i < 6; i++) {
            boolean allowed = system.checkMemberEntry(cardNumber, password);
            if (!allowed) {
                // 将会员加入黑名单
                system.blacklistManager.addToBlacklist(cardNumber);
                break;
            }
        }

        // 再次尝试进入超市
        boolean result = system.checkMemberEntry(cardNumber, password);
        System.out.println("Member " + cardNumber + " access result: " + result);
    }
}

四、总结与展望

通过上述设计和实现,我们构建了一个简单的会员管理系统,该系统能够有效防止“黄牛”现象的发生。当然,这只是一个基础版本,实际应用中还需要考虑更多的因素,例如与数据库的集成、并发处理、系统安全性等。

同时,我们也可以利用大数据和机器学习技术,对会员的消费行为进行更深入的分析和预测,从而进一步提高系统的准确性和可靠性。


配图(示意图)

+----------------------+
|  会员管理系统        |
+----------------------+
| 1. 会员验证模块       |
|    - 验证会员身份     |
+----------------------+
| 2. 消费监控模块       |
|    - 监控消费频率     |
|    - 异常消费警告     |
+----------------------+
| 3. 黑名单管理模块     |
|    - 添加黑名单       |
|    - 查询黑名单       |
|    - 删除黑名单       |
+----------------------+

希望这篇文章能够帮助你更好地理解如何用Java实现会员管理系统的防黄牛策略,并为你的项目开发提供灵感。
作者:
代老师的编程课
出处:
https://zthinker.com/
如果你喜欢本文,请长按二维码,关注
Java码界探秘
.
代老师的编程课

1.
背景

业务出现异常后,或者某个sql导致系统卡顿。需要问题后需要溯源,需要获取这个sql是在哪个客户端的IP发起的。

2.
cs架构

客户端直接连接数据库,可以很方便查询,采用通过sql_id找到客户端、进程或者port等,默认的模式是没有ip地址记录

select machine,program,port from GV$SESSION where sql_id= 
或者
select machine,program,port from GV$ACTIVE_SESSION_HISTORY where sql_id=

select machine,program,port from dba_hist_ACTIVE_SESS_HISTORY where sql_id=

再通过数据库监听查询或者应用服务器

2.1.
案例说明

select machine,program,port from GV$SESSION where sql_id=

或者 GV$ACTIVE_SESSION_HISTORY 的表获取到

通过获取machine、program、port这几个字段。

再通过数据库监听日志只获取

cat listener.log|grep 51880|grep Thin

注意:发现监控日志中记录的计算机的主机名和session中记录的可能不一致

3.
BS架构

bs架构和cs架构的不同点,bs采用多层架构,是应用服务连接数据库的,获取到的ip也是应用服务器的ip地址。

以java应用服务器为例,需要查询到客户端的ip发起端。

有个技巧:java服务的应用名称都是叫JDBC Thin Client,这样一台服务器中有多个java服务是无法区别的,可以手工区别名称来定义不同的java服务,如java应用的配置文件中,针对链接池配置,这样你的应用名称就变成了oracle-monitor

      data-source-properties:
        "[v$session.program]": oracle-monitor

有以下几种方式

  1. 通过nginx,客户端连接nginx,nginx转发到应用服务器中。再配置时间和请求的交易来定位,需要记录应用服务器的日志,日志格式类似
log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent $request_time $upstream_response_time "$http_referer" '
                      '$connection $upstream_addr "$http_x_forwarded_for" "$http_cookie" ';

$remote_addr:这个就是客户端的ip

  1. 没有nginx,应用容器采用tomcat,可以查看tomcat的请求日志

  1. 还有应用端记录登录日志来获取

4.
数据库登录触发器

登录的时候,可以再session记录ip地址,这样就不需要到监控日志中去查询具体的ip地址

CREATE OR REPLACE TRIGGER on_logon_trigger
AFTER LOGON ON DATABASE
BEGIN
    DBMS_APPLICATION_INFO.SET_CLIENT_INFO(SYS_CONTEXT('USERENV', 'IP_ADDRESS'));
END;

SELECT * FROM gv$session WHERE client_info 这个字段中就会体现ip地址

5.
总结

追溯源头的ip地址

  1. 通过慢的sql_id来获取客户端、应用、port,来定位ip地址。BS架构中应用服务名可以在链接池中设置不同的应用名。可以新增一个登录的触发器,记录一下ip更加方便
  2. BS架构需要定位客户端,需要通过请求和时间等,再通过nginx或者tomcat访问日志来定位

前言

本项目需要使用 SCSS 来编写页面样式。

Sass (Syntactically Awesome Stylesheets)是一个 css 预处理器,而 SCSS 是
Sass
的一种语法格式,它完全兼容 CSS,同时扩展了 CSS 的功能,使得样式编写更加高效、灵活和模块化。

对于初学者,SCSS 是完全和 CSS 兼容的,这意味着几乎为零的学习曲线。SCSS 语法即是:它只是加了一些功能的 CSS。当你和没经验的开发者一起工作时这很重要:他们可以很快开始编码而不需要首先去学习 Sass。

此外,SCSS 还是
易于阅读
的,因为它是有语义的,而不是用符号表示。当你读到
@mixin
,你就会知道这是一个 mixin 声明;当你看到
@include
,你就是在引用一个 mixin。他并没有用任何缩写,当你大声读出来时所有的都很明了。

还有,现在几乎所有 Sass 的工具,插件和 demo 都是基于 SCSS 语法来开发的。随着时间过去,SCSS 会变成大家首选的选择。比如,你现在很难找到一个 Sass 缩进语法的高亮插件,通常都只有 SCSS 的可以用。

本文以编写 404 页面为例,介绍 SCSS 的使用。

关于 SCSS

SCSS 相比起 CSS 的优势

  • 模块化
    :通过变量、嵌套和导入(
    @import

    @use
    )组织样式文件。
  • 可维护性
    :减少重复代码,便于维护和修改。
  • 兼容性
    :SCSS 文件可以完全兼容 CSS 文件,允许渐进式过渡。
  • 功能强大
    :支持逻辑判断(
    @if
    )、循环(
    @for
    )、和函数(
    @function
    )等功能。

相关资源

SCSS 的主要功能

变量(Variables)

可以使用变量存储颜色、字体大小等值,方便复用和管理。

// SCSS
$primary-color: #3498db;
$font-size: 16px;

body {
  color: $primary-color;
  font-size: $font-size;
}

转译后的 CSS:

body {
  color: #3498db;
  font-size: 16px;
}

嵌套(Nesting)

SCSS 支持嵌套语法,使样式层级结构更清晰,与 HTML 结构更接近。

// SCSS
nav {
  ul {
    margin: 0;
    li {
      display: inline-block;
      a {
        text-decoration: none;
        color: $primary-color;
      }
    }
  }
}

转译后的 CSS:

nav ul {
  margin: 0;
}
nav ul li {
  display: inline-block;
}
nav ul li a {
  text-decoration: none;
  color: #3498db;
}

混合(Mixins)

类似于函数,可以定义一段通用样式,并在多个地方调用。

// SCSS
@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

.container {
  @include flex-center;
  height: 100vh;
}

转译后的 CSS:

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

继承(Inheritance)

使用
@extend
来复用已有的样式。

// SCSS
.btn {
  padding: 10px 20px;
  border-radius: 5px;
}

.btn-primary {
  @extend .btn;
  background-color: $primary-color;
  color: white;
}

转译后的 CSS:

.btn {
  padding: 10px 20px;
  border-radius: 5px;
}
.btn-primary {
  padding: 10px 20px;
  border-radius: 5px;
  background-color: #3498db;
  color: white;
}

运算(Operations)

SCSS 支持基本的数学运算,例如加减乘除。

// SCSS
$base-padding: 10px;

.box {
  padding: $base-padding * 2;
  margin: $base-padding / 2;
}

转译后的 CSS:

.box {
  padding: 20px;
  margin: 5px;
}

安装

多年来 node-sass 一直是 JavaScript 社区里的主流选择,它实际上只是 libsass 在 node 环境下的一个 wrapper, 编译 sass 文件的实际工作是 libsass 完成的。

在使用 node-sass 过程中遇到的很多问题实际上也是 libsass 引发的,libsass 是用 C/C++ 实现的,常见的问题是,在安装 node-sass 的过程中经常会出现安装失败的情况,又或者切换了 Node.js 版本发现 node-sass 需要重新安装才能用,如果你在 docker 中安装 node-sass 还会遇到由于缺少各种依赖导致 node-sass build 失败的情况,又或者在国内由于网络原因导致 node-sass 需要的二进制文件下载不下来而 build 失败。

现在,sass 官方已经使用 dart-sass 作为 sass 的主要实现:

Dart Sass is the primary implementation of Sass, which means it gets new features before any other implementation. It’s fast, easy to install, and it compiles to pure JavaScript which makes it easy to integrate into modern web development workflows.

所以安装命令如下:

yarn add sass --dev
yarn add sass-loader@7.1.0 --dev

注意 Vue2 使用的 webpack 版本是 3.6.0,所以这里的
sass-loader
必须使用 7.1.0 版本

(我被
TypeError: this.getOptions is not a function
这个报错折腾了大半天才搞定)

配置

编辑
build/webpack.base.conf.js
文件下
module
里的
rules
标签,添加以下配置

{
    test: /\.scss$/,
    loaders: ['style', 'css', 'sass']
}

使用方法

有 Webpack 的用法

本项目是基于 Vue 的 SPA(单页应用),编写代码之后经过 webpack 打包成 HTML、CSS、JavaScript,前面的步骤也配置好了 SASS,可以直接在 .vue 文件里写 SCSS。

在页面代码 style 标签中把 lang 设置成 scss 即可。

<style scoped lang="scss">

</style>

这里我改了一下
src/views/404.vue
页面

<template>
  <div class="site-wrapper site-page--not-found">
    <div class="site-content__wrapper">
      <div class="site-content">
        <h2 class="not-found-title">404</h2>
        <p class="not-found-desc">抱歉!您访问的页面<em>失联</em>啦 ...</p>
        <el-button @click="$router.go(-1)">返回上一页</el-button>
        <el-button type="primary" class="not-found-btn-gohome" @click="$router.push('/')">进入首页</el-button>
      </div>
    </div>
  </div>
</template>

样式代码如下

.site-wrapper.site-page--not-found {
  position: absolute;
  top: 60px;
  right: 0;
  bottom: 0;
  left: 0;
  overflow: hidden;

  .site-content__wrapper {
    padding: 0;
    margin: 0;
    background-color: #fff;
  }

  .site-content {
    position: fixed;
    top: 15%;
    left: 50%;
    z-index: 2;
    padding: 30px;
    text-align: center;
    transform: translate(-50%, 0);
  }

  .not-found-title {
    margin: 20px 0 15px;
    font-size: 8em;
    font-weight: 500;
    color: rgb(55, 71, 79);
  }

  .not-found-desc {
    margin: 0 0 30px;
    font-size: 26px;
    text-transform: uppercase;
    color: rgb(118, 131, 143);

    > em {
      font-style: normal;
      color: #ee8145;
    }
  }

  .not-found-btn-gohome {
    margin-left: 30px;
  }
}

改完之后的页面效果

不错,可以收工了~

没有 Webpack 的用法

如果想单独使用 SCSS,可以直接调用 SASS 编译:

sass styles.scss styles.css

然后在 HTML 里引入

<link rel="stylesheet" href="styles.css">

参考资料

前言:
本文主要记录了基于低版本gitlab(v3 api)实现in-line comment功能的过程中踩过的坑及相应的解决方案,理论上其他低版本gitlab不具备的API都可以参照此类方法进行实现(只要能通过页面操作抓取到相关接口),从而让低版本gitlab焕发新生~

背景

近期我们落地了AI Code review实践,在CICD流程中当merge request创建时,会基于MR的diff代码进行AICR并将问题以comment的形式提交,如下图所示:

目前我们希望进一步改进体验:除了整体的comment,也希望能将所有问题分别以in-line comment的形式插入到对应代码片段的对应行号中,以便形成研发CR时边看代码边看问题的无缝体验(无需频繁在Discussion问题列表和代码Changes面板之间切换)

但因为一些历史原因,我们公司目前还存在新旧两套gitlab,其中旧的gitlab版本较低(8.16 v3 API),没有现成的in-line comment API。下文将分为两个阶段来陈述如何在低版本gitlab上实现in-line comment功能以及如何兼容一些特殊场景以增强用户体验

第一阶段
(初步实现功能)

需求:

根据AI返回的行号,将对应问题in-line comment到代码changes面板上,方便开发CR代码时就能看到相关问题,期望效果如下图所示:

问题:

低版本gitlab没有in-line comment API

解决方案:

通过抓取页面操作相关接口,最终通过模拟登录态并获取所有所需参数,从而构造出与在web页面上操作等价的in-line comment请求

解决过程:

1)接口分析

通过抓取web页面上的in-line comment请求,发现有一个notes请求。经过分析与回放尝试,发现有如下必传参数是需要根据情况动态获取的:

  • merge_request_diff_head_sha  (head_sha)
  • target_id  (Merge Request 的唯一 ID)
  • note[noteable_id]  (与target_id一致)
  • note[position] (新/旧文件路径、base_sha、start_sha、head_sha、new_line、old_line等;其中因为new_line、old_line踩的坑最多,下文会提到)
  • note[note] (评论的内容)

2)参数获取

  • base_sha、start_sha、head_sha 等参数可以通过 `/projects/:id/merge_requests/:merge_request_id/versions` 接口获取
  • old_path、new_path等参数可以通过 `/projects/:id/merge_requests/:merge_request_id/changes` 接口获取
  • authenticity_token 需要访问被操作页面,然后从页面中解析获取(可结合BeautifulSoup对页面结构进行解析)
  • 同时还需要构造登录态(基于有权限的账号登录后保存session,然后结合requests.Session()进行后续请求)
  • new_line这一阶段是直接使用AI提供的行号(old_line没有传,因为测试场景是纯新增代码old_line不传也能成功,后来才发现是坑)

3)请求构造

按相关结构组装这些参数,构造与页面操作等价的in-line comment请求。

至此,便完成了基础的in-line comment功能

主要代码:

  • in-line comment请求
    def gitlab_v3_in_line_comment(self, project_id, mr_id, mr_iid, comment, file_name, old_line, new_line):
        '''gitlab低版本无现成的in-line comment API,故通过页面抓取的接口曲线进行(需要模拟登录态并构造相关参数)
        comment: 要备注的内容,即代码问题描述
        file_name: AI返回的文件名
        code_line: AI返回的行号
        '''
        # 获取gitlab session
        session = self.gitlab_v3_get_login_session()
        # Step 3: 重新提取评论页面的 authenticity_token(不能沿用登录页面的authenticity_token)
        path_with_namespace = self.gitlab_get_project_by_id(project_id).get('path_with_namespace', '')
        comment_page = session.get(f"https://git.kugou.net/{path_with_namespace}/merge_requests/{mr_iid}")
        soup = BeautifulSoup(comment_page.text, "html.parser")
        authenticity_token = soup.find("input", {"name": "authenticity_token"})["value"]
        # print("MR页面authenticity_token", authenticity_token)
 
        # step 4: 获取MR相关信息
        base_sha, start_sha, head_sha = self.gitlab_v3_get_mr_diff_versions(project_id, mr_id)  # 获取MR的diff版本信息
        # 根据AI返回的文件名,获取old_path、new_path
        result = self.gitlab_v3_get_single_mr_changes(project_id, mr_id, file_name)
        if result:
            old_path = result[0].get('old_path', '')
            new_path = result[0].get('new_path', '')
        else:
            old_path = new_path = file_name
 
        # Step 5: 构造评论请求
        comment_url = f"https://git.kugou.net/{path_with_namespace}/notes"
        comment_payload = {
            "utf8": "✓",
            "authenticity_token": authenticity_token,
            "view": "inline",
            "line_type": "",    # 新增代码传new,已有代码传空(但是都为空也能成功)
            "merge_request_diff_head_sha": head_sha,
            "target_type": "merge_request",
            "target_id": f"{mr_id}",  # Merge Request 的唯一 ID
            "note[commit_id]": "",
            # "note[line_code]": "25d1157f6eee34be77947760c3d83e1f34efeb31_235_238",    # 页面锚点id_oldline_newline,非必须参数
            "note[noteable_id]": f"{mr_id}",   # [必须]与 target_id 一致
            "note[noteable_type]": "MergeRequest",
            "note[type]": "DiffNote",
            # "note[position]": '{"old_path":"GuessSongActivityService.java","new_path":"GuessSongActivityService.java","new_line":55,"base_sha":"29f230c853e957e049c7ef3cf8ba7435f82479ef","start_sha":"29f230c853e957e049c7ef3cf8ba7435f82479ef","head_sha":"3160984ed116026742a911ec7d8cf332e4fd4c3c"}',
            "note[position]": json.dumps({
                "old_path": old_path,   # 旧文件路径,通过gitlab_v3_get_single_mr_changes接口获取
                "new_path": new_path,   # 新文件路径,同上
                "old_line": old_line,   # 若是新旧行号不一致的情况,old_line/new_line均必传,否则会报错(采用页面解析方案获取,第一阶段未传该参数踩坑了)
                "new_line": new_line,  # 针对新增的代码进行in-line comment,只需要传入该参数即可,old_line可不传
                "base_sha": base_sha,   # MR的diff版本信息,通过gitlab_v3_get_mr_diff_versions接口获取
                "start_sha": start_sha,  # 与base_sha获取方法一致
                "head_sha": head_sha  # 与base_sha获取方法一致
            }),
            "note[note]": comment,   # 具体的评论内容
            "commit": "Comment"
        }
        # print('\n -----构造的评论参数----- ', comment_payload)
 
        # Step 6: 发送评论请求
        response = session.post(comment_url, data=comment_payload)
        # print('评论结果 : ', response.status_code, response.text)
        return response
  • gitlab session处理
    def gitlab_v3_get_login_session(self):
        '''
        gitlab登录态
        若缓存中的session数据未失效,则直接从缓存中获取相关数据并构造session
        若已失效,则重新登录获取session,并缓存
        '''
        SESSION_CACHE_KEY = "gitlab_session_for_AICR"
        SESSION_TIMEOUT = 3600 * 8
 
        # 获取已认证的session,如果缓存存在则直接使用
        session_data = cache.get(SESSION_CACHE_KEY)
        if session_data:
            # 从缓存恢复session
            session = requests.Session()
            session.cookies.update(session_data.get("cookies", {}))
            session.headers.update(session_data.get("headers", {}))
            return session
        
        # 如果缓存不存在,重新登录并缓存
        # Step 1: 获取登录页面并提取 authenticity_token
        login_url = "https://git.kugou.net/users/sign_in"
        session = requests.Session()
        login_page = session.get(login_url)
        soup = BeautifulSoup(login_page.text, "html.parser")
        auth_token_input = soup.find('input', {'name': 'authenticity_token', 'type': 'hidden'})
        if auth_token_input:
            authenticity_token = auth_token_input['value']
        else:
            return ''
 
        # Step 2: 登录
        login_payload = {
            "user[login]": settings.AICR_USER,  # AI-CodeReviewer用户名
            "user[password]": settings.AICR_PASS,  # AI-CodeReviewer密码
            "authenticity_token": authenticity_token  # 登录页面的authenticity_token
        }
        response = session.post(login_url, data=login_payload)
        # 检查是否登录成功(根据 GitLab 的页面返回内容或状态码判断)
        if response.status_code != 200 or "Sign in" in response.text:
            logger.error("gitlab Login failed. Please check credentials.")
        else:
            # 缓存session(Session对象无法直接序列化,需要提取cookies和headers,以便后续构造)
            session_data = {
                "cookies": session.cookies.get_dict(),
                "headers": dict(session.headers)
            }
            cache.set(SESSION_CACHE_KEY, session_data, timeout=SESSION_TIMEOUT)
        return session

第二阶段
(特殊场景兼容)

问题:

第一阶段功能上线运行一段时间后,我们发现很多AICR问题没有成功提交in-line comment,经过分析主要是以下两种原因:

  • AI返回的行号在gitlab MR changes面板上被隐藏折叠了(我们默认情况下是没法在web页面选中这个行号的,自然也是没法基于该行号提交comment)
  • 针对变更的代码,会存在old_line、new_line不一致的场景,这种情况下需要同时传对新/旧行号才能成功添加comment(而新旧行号只有页面上能获取到,也没有对应api可以获取)

预期需求:

针对上述问题,我们希望达成如下效果,以改进用户体验

1)当AI返回的行号在MR changes面板被隐藏折叠时,将相关AICR问题备注到最相邻的那一行,如下所示

2)针对变更代码 old_line、new_line不一致的情况,获取页面上实际展示的新/旧行号构造请求

解决方案:

1)构造登录态请求diffs.json接口,拿到所有diff文件的html源码

2)结合BeautifulSoup解析步骤1拿到的html源码,找到对应文件的div,然后拿到所有行号的data-position信息

3)构造line_mapping_dict,用来判断传入的行号参数的有效性,并做兼容性处理

解决过程:

1)页面分析

MR Changes面板每一个文件就是一个大的div块,其中每一行代码对应一个tr,然后每个tr中有相关行号信息,我们只需要解析整个页面然后获取相关行号即可

过程中还有一个小插曲:实际应用中发现行号在MR页面上不展示的情况下,对应in-line comment的行号参数也必须传null,如果传了<tr>标签中展示的原行号也会报错... 故继续在页面上找规律,发现在<td>标签中的data-position属性符合需求(页面上不展示的行号其值为null),故进一步调整解析逻辑

2)动态接口信息获取及解析

另外过程中还发现MR的change页面不是一个纯静态页面,代码diff信息是通过接口动态获取的。故没法直接通过访问MR的URL来拿到代码diff内容,需要调用相应的diffs.json接口获取,所以我们需要解析该接口的返回内容(好在接口返回内容也是html格式,可以结合BeautifulSoup快速处理)

3)根据AI给定的行号判定有效的新/旧行号

将每个文件在页面上展示的新/旧行号都存储下来,然后基于AI给定的行号进行有效性判定:

  • 若给定的行号不存在,则返回最页面上展示的最相邻的新/旧行号
  • 若存在,则直接返回对应的新/旧行号

踩完各种坑之后,最终效果如下:

主要代码:

  • gitlab MR changes页面-代码片段行号信息获取
    def get_mr_changefile_display_lines(self, project_id, mr_iid, file_name):
        '''
        获取MR changes板块对应文件的所有展示出来的行号,用于后续处理兼容以下特殊情况:
        1. AI返回的行号在MR changes板块被折叠隐藏
        2. 修改代码的情况下,代码行号发生变化,in-line comment的行号需要同时传对应的old_line和new_line
        return: line_mapping_dict  eg: {4: (4, 4), 5: (5, 5), 357: (357, 360), 360: (357, 360)}
        '''
        session = self.gitlab_v3_get_login_session()
        path_with_namespace = self.gitlab_get_project_by_id(project_id).get('path_with_namespace', '')
        resp = session.get(f"https://git.kugou.net/{path_with_namespace}/merge_requests/{mr_iid}/diffs.json")
        html_content = resp.json().get('html', '')
 
 
 
        # 使用 BeautifulSoup解析HTML片段
        soup = BeautifulSoup(html_content, 'html.parser')
 
        # 定义目标文件的关键字
        target_keyword = f"{file_name}/diff"  # 如 services/numeric/numeric_service.go/diff
        # 查找 data-blob-diff-path属性包含特定字符串的 div标签
        target_div = soup.find(
            'div',
            attrs={"data-blob-diff-path": lambda value: value and target_keyword in value}
        )
        # 如果找到目标div,则进一步解析
        if target_div:
            # 查找该div下符合条件的所有class包含line_content和noteable_line的<td>,形如<td class="line_content new noteable_line old">
            td_elements = target_div.find_all(
                'td',
                attrs={'class': lambda x: x and set(['line_content', 'noteable_line']).issubset(x.split())}
            )
            # 提取所有行号,并构造行号映射字典,形如:{4: (4, 4), 5: (5, 5), 357: (357, 360), 360: (357, 360)}
            line_mapping_dict = {}
            for td in td_elements:
                # 获取 data-position 属性
                data_position = td.get('data-position')
                if data_position:
                    try:
                        # 将 JSON 字符串解析为字典
                        position_data = json.loads(data_position)
                        old_line = position_data.get('old_line')
                        new_line = position_data.get('new_line')
                        # 以old_line、new_line作为key分别存储一次,方便后续可以根据任意匹配行号查询到相应的old_line、new_line对
                        if old_line: line_mapping_dict[old_line] = (old_line, new_line)  # 避免存这种 None: (None, 357)
                        if new_line: line_mapping_dict[new_line] = (old_line, new_line)
                    except json.JSONDecodeError:
                        logger.error("Failed to decode JSON in data-position: %s", data_position)
            # print(line_mapping_dict)
            return line_mapping_dict
        else:
            # print(f"未找到包含{target_keyword}的div")
            return None

  • 行号有效性判定及兼容处理逻辑
def find_line_mapping(line_number, line_mapping_dict):
    """
    根据AI提供的行号在line_mapping_dict中查找相关联的新/旧行号。
    如果行号存在,直接返回;如果不存在,返回最接近的行号。
    return: (old_line, new_line)
    """
    if not line_number or not line_mapping_dict:
        return None
    # print("line_number : ", line_number)
    if line_number in line_mapping_dict:
        return line_mapping_dict[line_number]
    # 获取所有行号并排序
    sorted_lines = sorted(line_mapping_dict.keys())
    # 找到最接近的行号
    closest_line = None
    min_diff = float('inf')  # 初始设置为无穷大,以便首次比较
    for line in sorted_lines:
        diff = abs(line - line_number)
        if diff < min_diff:
            min_diff = diff
            closest_line = line
    return line_mapping_dict[closest_line]

以上就是在低版本gitlab上实现in-line comment全过程了,希望能对也有此类需求的朋友提供一个参考思路