Spring Boot 7 微服务执行Bot代码(传递路线是难点)

在这里插入图片描述

本节课也是新开一个微服务:BotRunningSystem
红色为本次实现:
在这里插入图片描述
代码执行,此项目只支持java代码的执行,用的是joor java 8实现。

可扩展为docker实现,设置内存上限,时间,用命令可执行所有语言代码,并具备一定安全性,因为docker与运行环境隔绝。

1.让BotRunning System获得到前端选择的Bot

1.1.新建Bot执行微服务项目

右键backendcloud->新建->maven

1.2.修改pom依赖

1.复制粘贴matchingsystem的pom中的所有依赖
2.添加新依赖 joor-java-8 :可以在Java中动态编译Java代码

1.3.BotRunningSystem接收前端选择的botId

文件结构

botrunningsystem
    config
        RestTemplateConfig.java
        SecurityConfig.java
    controller
        BotRunningController.java
    service
        impl
            BotRunningServiceImpl.java
        BotRunningService.java
    BotRunningSystemApplication.java

将BotRunningSystem/Main.java 更名为 BotRunningSystemApplication.java

package com.kob.botrunningsystem;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BotRunningSystemApplication {
    public static void main(String[] args) {
        SpringApplication.run(BotRunningSystemApplication.class,args);
    }
}

接口

package com.kob.botrunningsystem.service;

public interface BotRunningService {
    public String addBot(Integer userId,String botCode,String input);
}

接口实现

package com.kob.botrunningsystem.service.impl;

import com.kob.botrunningsystem.service.BotRunningService;
import org.springframework.stereotype.Service;

@Service
public class BotRunningServiceImpl implements BotRunningService {
    @Override
    public String addBot(Integer userId, String botCode, String input) {
        System.out.println("add bot: " + userId + " " + botCode + " " + input);
        return "add bot success";
    }
}

控制器

package com.kob.botrunningsystem.controller;

import com.kob.botrunningsystem.service.BotRunningService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;

@RestController
public class BotRunningController {
    @Autowired
    private BotRunningService botRunningService;

    @PostMapping("/bot/add/")
    public String addBot(@RequestParam MultiValueMap<String,String> data){
        Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
        String botCode = data.getFirst("bot_code");
        String input = data.getFirst("input");
        return botRunningService.addBot(userId,botCode,input);
    }
}

权限控制(网关)

package com.kob.botrunningsystem.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/bot/add/").hasIpAddress("127.0.0.1")//新加
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();
    }
}

服务间发送消息的RestTemplate

package com.kob.botrunningsystem.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

端口配置
在resources新建文件

1.4.前端选择Bot+发送bot_id给后端

匹配界面
添加选择操作方式
在这里插入图片描述

<template>
  <div class="matchground">
    <div class="row">
        
        <!-- 自己 -->
        <div class="col-4">
            <div class="user-photo">
                <img :src="$store.state.user.photo" alt="">
            </div>
            <div class="user-username">
                {{$store.state.user.username}}
            </div>
        </div>
        <!-- 选择Bot -->
        <div class="col-4">
            <div class="user-select-bot">
                <select class="form-select" aria-label="Default select example" v-model="select_bot">
                    <option value="-1" selected>亲自出马</option>
                    <option v-for="bot in bots" :key="bot.id" :value="bot.id">{{ bot.title }}</option>
                </select>
            </div>
        </div>
        <!-- 对手 -->
        <div class="col-4">
            <div class="user-photo">
                <img :src="$store.state.pk.opponent_photo" alt="">
            </div>
            <div class="user-username">
                {{$store.state.pk.opponent_username}}
            </div>
        </div>

        <div class="col-12" style="text-align : center;  padding-top : 12vh;">
            <button type="button" class="btn btn-warning btn-lg" @click="click_match_btn">{{match_btn_info}}</button>
        </div>
        
    </div>
  </div>
</template>

<script>
import { ref } from "vue"
import { useStore } from "vuex"
import $ from "jquery"

export default {
    setup(){
        const store = useStore();
        let match_btn_info = ref("开始匹配")
        let bots = ref([])
        let select_bot = ref(-1)

        const click_match_btn = ( )=>{
            if(match_btn_info.value === "开始匹配"){
                match_btn_info.value = "取消";
                
                //向后端发送信息
                store.state.pk.socket.send(JSON.stringify({
                    event:"start-matching",
                    bot_id: select_bot.value,
                }));
            } else if(match_btn_info.value === "取消"){
                match_btn_info.value = "开始匹配";
                
                store.state.pk.socket.send(JSON.stringify({
                    event:"stop-matching",
                }));
            }
        }

        const refresh_bots = () => {
            $.ajax({
                url: "http://127.0.0.1:3000/user/bot/getlist/",
                type: "get",
                headers: {
                    'Authorization': "Bearer " + store.state.user.token,
                },
                success(resp) {
                    bots.value = resp;
                }
            })
        };
        refresh_bots();

        return {
            match_btn_info,
            click_match_btn,
            bots,
            select_bot,
        }
    }
}
</script>

<style scoped>

div.matchground {
    width: 60vw;
    height: 70vh;
    background-color:rgba(50 ,50 ,50 ,0.5);
    margin: 40px auto;
}
div.user-photo {
    text-align: center;
    padding-top: 10vh;
}
div.user-photo > img{
    border-radius: 50%;
    width: 20vh;
}
div.user-username {
    text-align: center;
    font-size: 20px;
    font-weight: 600;
    color: white;
    margin-top: 2vh;
}
div.user-select-bot {
    padding-top: 20vh;
}
div.user-select-bot > select {
    width: 60%;
    margin: 0 auto;
}

</style>

1.5.后端接收bot

backend接收Bot

在这里插入图片描述

WebSocketServer接收到匹配请求,将bot传给匹配服务
在这里插入图片描述
在这里插入图片描述

Matching System接收Bot

在这里插入图片描述

Matching System接收到backend传的botId,将bot传给BotRunningSystem服务
控制器
在这里插入图片描述
接口
在这里插入图片描述
实现接口
在这里插入图片描述

匹配池

在这里插入图片描述
在这里插入图片描述

匹配池的Player

在这里插入图片描述

匹配池返回结果加上botId

在这里插入图片描述
在这里插入图片描述

StartGameController.java

在这里插入图片描述

在这里插入图片描述

StartGameService.java

在这里插入图片描述

StartGameServiceImpl.java

在这里插入图片描述

WebSocketServer.java

在这里插入图片描述
在这里插入图片描述

Player.java

在这里插入图片描述

WebSocketServer.java

将RestTemplate变成public,若是代码输入则屏蔽人的输入
在这里插入图片描述

Game.java

在这里插入图片描述

private final Player playerA,playerB;
private final static String addBotUrl = "http://127.0.0.1:3002/bot/add/";

public Game(
        Integer rows,
        Integer cols,
        Integer inner_walls_count,
        Integer idA,
        Bot botA,
        Integer idB,
        Bot botB
) {
    this.rows = rows;
    this.cols = cols;
    this.inner_walls_count = inner_walls_count;
    this.g = new int[rows][cols];
    Integer botIdA = -1,botIdB = -1;
    String botCodeA = "",botCodeB = "";
    if(botA != null){
        botIdA = botA.getId();
        botCodeA = botA.getContent();
    }
    if(botB != null){
        botIdB = botB.getId();
        botCodeB = botB.getContent();
    }
    playerA = new Player(idA,botIdA,botCodeA,this.rows - 2, 1, new ArrayList<>());
    playerB = new Player(idB,botIdB,botCodeB,1, this.cols - 2, new ArrayList<>());
}



//获得input
private String getInput(Player player){
    Player me,you ;
    if(playerA.getId().equals(player.getId())){
        me = playerA ;
        you = playerB ;
    } else {
        me = playerB ;
        you = playerA ;
    }
    return getMapString() + "#" +
            me.getSx() + "#" +
            me.getSy() + "#(" +
            me.getStepsString() + ")#" +
            you.getSx() + "#" +
            you.getSy() + "#(" +
            you.getStepsString() + "#)" ;
}

private void sendBotCode(Player player){
    if(player.getBotId().equals(-1)) return ;//亲自出马
    MultiValueMap<String,String> data = new LinkedMultiValueMap<>();
    data.add("user_id",player.getId().toString());
    data.add("bot_code",player.getBotCode());
    data.add("input",getInput(player));
    WebSocketServer.restTemplate.postForObject(addBotUrl,data,String.class);
}

private boolean nextStep(){//两名玩家的下一步
    try {
        Thread.sleep(200);//因为前端走一格200ms
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    sendBotCode(playerA);
    sendBotCode(playerB);
......
}

private String getMapString(){
    StringBuilder res = new StringBuilder();
    for(int i=0;i<rows;i++){
        for(int j=0;j<cols;j++){
            res.append(g[i][j]);
        }
    }
    return res.toString();
}

成功
在这里插入图片描述

1.6.总结:实现了BotId传送到BotRunningSystem系统

在这里插入图片描述

2.BotRunning System的实现

2.1思路:生产者消费者模型

在这里插入图片描述

2.2文件结构

BotRunningSystem
    service
        impl
            utils
                Bot.java
                BotPool.java
                Consumer.java
    utils
        Bot.java
        BotInterface.java

2.3Bot的实现

package com.kob.botrunningsystem.service.impl.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Bot {
    private Integer userId;
    private String botCode;
    private String input;
}

2.4BotPoll的实现

虽然队列没用消息队列,但是因为我们写了条件变量与锁的操作,所以等价于消息队列

package com.kob.botrunningsystem.service.impl.utils;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BotPool extends Thread{

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();//条件变量
    private final Queue<Bot> bots = new LinkedList<>();//消息队列-->add,remove-->锁

    //添加一个任务-Bot
    public void addBot(Integer userId,String botCode,String input){
        lock.lock();//涉及到bots
        try {
            bots.add(new Bot(userId,botCode,input));
            condition.signalAll();
        } finally {
            lock.unlock();
        }

    }
    //消费一个任务
    private void consume(Bot bot){
        Comsumer comsumer = new Comsumer();
        comsumer.startTimeut(2000,bot);
    }

    @Override
    public void run() {
        while (true){
            lock.lock();
            if(bots.isEmpty()){//空
                try {
                    condition.await();//【消息队列空】阻塞当前线程,直到被唤醒(默认包含解锁)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    lock.unlock();
                    break;
                }
            } else {
                Bot bot = bots.remove();//取出当前任务+移除
                lock.unlock();
                consume(bot);//消耗任务,用时长,执行代码
            }
        }
    }
}

2.5BotRunningServiceImpl.java

加任务

package com.kob.botrunningsystem.service.impl;

import com.kob.botrunningsystem.service.BotRunningService;
import com.kob.botrunningsystem.service.impl.utils.BotPool;
import org.springframework.stereotype.Service;

@Service
public class BotRunningServiceImpl implements BotRunningService {
6    public final static BotPool botPool = new BotPool();

    @Override
    public String addBot(Integer userId, String botCode, String input) {
        System.out.println("add bot: " + userId + " " + botCode + " " + input);
        botPool.addBot(userId,botCode,input);
        return "add bot success";
    }
}

2.6BotPool线程的启动

package com.kob.botrunningsystem;

import com.kob.botrunningsystem.service.impl.BotRunningServiceImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BotRunningSystemApplication {
    public static void main(String[] args) {
        BotRunningServiceImpl.botPool.start();//启动线程
        SpringApplication.run(BotRunningSystemApplication.class,args);
    }
}


2.7BotInterface.java

用户写Bot实现的接口

package com.kob.botrunningsystem.utils;

public interface BotInterface {
    public Integer nextMove(String input);//下一步方向
}

2.8Bot.java

package com.kob.botrunningsystem.utils;

public class Bot implements com.kob.botrunningsystem.utils.BotInterface {
    @Override
    public Integer nextMove(String input) {
        return 0;
    }
}

2.9Consumer的实现

package com.kob.botrunningsystem.service.impl.utils;

import com.kob.botrunningsystem.utils.BotInterface;
import org.joor.Reflect;

import java.util.UUID;

public class Comsumer extends Thread{
    private Bot bot;
    public void startTimeut(long timeout,Bot bot){
        this.bot = bot;
        this.start();
        try {
            this.join(timeout);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            this.interrupt();
        }
    }

    private String addUid(String code,String uid){
        int k = code.indexOf(" implements com.kob.botrunningsystem.utils.BotInterface");
        return code.substring(0,k) + uid +code.substring(k);
    }

    @Override
    public void run() {
        UUID uuid = UUID.randomUUID();
        String uid = uuid.toString().substring(0,8);

        BotInterface botInterface = Reflect.compile(
                "com.kob.botrunningsystem.utils.Bot" + uid,
                addUid(bot.getBotCode(),uid)
        ).create().get();
        Integer direction = botInterface.nextMove(bot.getInput());

        System.out.println("move-direction: " + bot.getUserId() + " " + direction);
    }
}

2.10BotPool.java

在这里插入图片描述

2.11测试

package com.kob.botrunningsystem.utils;

public class Bot implements com.kob.botrunningsystem.utils.BotInterface {
    @Override
    public Integer nextMove(String input) {
        return 0;
    }
}

package com.kob.botrunningsystem.utils;

public class Bot implements com.kob.botrunningsystem.utils.BotInterface {
    @Override
    public Integer nextMove(String input) {
        return 2;
    }
}

在这里插入图片描述

2.12小Bug

在游戏结束后,点到其他页面再点回pk页面,结果没有消失
在这里插入图片描述

3.将Bot执行结果传给前端

3.1.BackEnd接收Bot代码的结果

在这里插入图片描述
文件结构

backend
    controller
        pk
            ReceiveBotMoveController.java
    service
        impl
            pk
                ReceiveBotMoveServiceImpl.java
        pk
            ReceiveBotMoveService.java

接口

package com.kob.backend.service.pk;

public interface ReceiveBotMoveService {
    public String receiveBotMove(Integer userId,Integer direction);
}

WebSocketServer操作类
在这里插入图片描述

接口实现

package com.kob.backend.service.impl.pk;

import com.kob.backend.consumer.WebSocketServer;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.service.pk.ReceiveBotMoveService;
import org.springframework.stereotype.Service;

@Service
public class ReceiveBotMoveServiceImpl implements ReceiveBotMoveService {
    @Override
    public String receiveBotMove(Integer userId, Integer direction) {

        System.out.println("receive bot move: " + userId + " " +direction);
        if(WebSocketServer.users.get(userId)!=null){
            Game game = WebSocketServer.users.get(userId).game;
            if(game != null){
                if(game.getPlayerA().getId().equals(userId)){//当前链接是A用户
                    game.setNextStepA(direction);
                } else if(game.getPlayerB().getId().equals(userId)){//当前链接是B用户
                    game.setNextStepB(direction);
                }
            }
        }
        return "receive bot move success";
    }
}

控制器

package com.kob.backend.controller.pk;

import com.kob.backend.service.pk.ReceiveBotMoveService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;

@RestController
public class ReceiveBotMoveController {
    @Autowired
    private ReceiveBotMoveService receiveBotMoveService;

    @PostMapping("/pk/receive/bot/move/")
    public String receiveBotMove(@RequestParam MultiValueMap<String,String> data){
        System.out.println("+++++++++++++++++++++++++++++++");
        Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
        Integer direction = Integer.parseInt(Objects.requireNonNull(data.getFirst("direction")));
        return receiveBotMoveService.receiveBotMove(userId,direction);
    }
}

权限控制(网关)

//实现用户密码的加密存储。==>加上此文件,必须是密文存储
//实现公开页面
package com.kob.backend.config;

import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/user/account/token/", "/user/account/register/").permitAll()
                .antMatchers("/pk/start/game/","/pk/receive/bot/move/").hasIpAddress("127.0.0.1")
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/websocket/**");//放行这一类链接
    }
}

3.2.BotRunningSystem返回Bot执行结果

Consumer.java



package com.kob.botrunningsystem.service.impl.utils;

import com.kob.botrunningsystem.utils.BotInterface;
import org.joor.Reflect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.UUID;

@Component
public class Consumer extends Thread{

    private static RestTemplate restTemplate ;
    @Autowired
    public void setRestTemplate(RestTemplate restTemplate){
        Consumer.restTemplate = restTemplate;
    }
    private Bot bot;
    private final static String receiveBotMoveUrl = "http://127.0.0.1:3000/pk/receive/bot/move/";


    public void startTimeout(long timeout,Bot bot){
        this.bot = bot;
        this.start();
        try {
            this.join(timeout);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            this.interrupt();
        }
    }

    private String addUid(String code,String uid){
        int k = code.indexOf(" implements com.kob.botrunningsystem.utils.BotInterface");
        System.out.println(k);
        return code.substring(0,k) + uid +code.substring(k);
    }

    @Override
    public void run() {
        UUID uuid = UUID.randomUUID();
        String uid = uuid.toString().substring(0,8);

        BotInterface botInterface = Reflect.compile(
                "com.kob.botrunningsystem.utils.Bot" + uid,
                addUid(bot.getBotCode(),uid)
        ).create().get();

        Integer direction = botInterface.nextMove(bot.getInput());

        System.out.println("move-direction: " + bot.getUserId() + " " + direction);

        MultiValueMap<String,String> data = new LinkedMultiValueMap<>();
        data.add("user_id",bot.getUserId().toString());
        data.add("direction",direction.toString());

        restTemplate.postForObject(receiveBotMoveUrl,data,String.class);
    }
}


测试

y总bot代码:

package com.kob.botrunningsystem.utils;

import java.util.ArrayList;
import java.util.List;

public class Bot implements com.kob.botrunningsystem.utils.BotInterface {
    static class Cell {
        public int x, y;
        public Cell(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    private boolean check_tail_increasing(int step) {  // 检验当前回合,蛇的长度是否增加
        if (step <= 10) return true;
        return step % 3 == 1;
    }

    public List<Cell> getCells(int sx, int sy, String steps) {
        steps = steps.substring(1, steps.length() - 1);
        List<Cell> res = new ArrayList<>();

        int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
        int x = sx, y = sy;
        int step = 0;
        res.add(new Cell(x, y));
        for (int i = 0; i < steps.length(); i ++ ) {
            int d = steps.charAt(i) - '0';
            x += dx[d];
            y += dy[d];
            res.add(new Cell(x, y));
            if (!check_tail_increasing( ++ step)) {
                res.remove(0);
            }
        }
        return res;
    }

    @Override
    public Integer nextMove(String input) {
        String[] strs = input.split("#");
        int[][] g = new int[13][14];
        for (int i = 0, k = 0; i < 13; i ++ ) {
            for (int j = 0; j < 14; j ++, k ++ ) {
                if (strs[0].charAt(k) == '1') {
                    g[i][j] = 1;
                }
            }
        }

        int aSx = Integer.parseInt(strs[1]), aSy = Integer.parseInt(strs[2]);
        int bSx = Integer.parseInt(strs[4]), bSy = Integer.parseInt(strs[5]);

        List<Cell> aCells = getCells(aSx, aSy, strs[3]);
        List<Cell> bCells = getCells(bSx, bSy, strs[6]);

        for (Cell c: aCells) g[c.x][c.y] = 1;
        for (Cell c: bCells) g[c.x][c.y] = 1;

        int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
        for (int i = 0; i < 4; i ++ ) {
            int x = aCells.get(aCells.size() - 1).x + dx[i];
            int y = aCells.get(aCells.size() - 1).y + dy[i];
            if (x >= 0 && x < 13 && y >= 0 && y < 14 && g[x][y] == 0) {
                return i;
            }
        }

        return 0;
    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿斯卡码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值