博客之QQ登录功能(一)

news2025/1/9 16:10:05

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

上图spring social 封装了1-8步需要的工作
在这里插入图片描述

1、新建包和书写配置文件

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

public class QQProperties {

	//App唯一标 识
	private String appId = "100550231";
	private String appSecret = "69b6ab57b22f3c2fe6a6149274e3295e";
	
	//QQ供应商
	private String providerId = "callback.do";
	//拦截器拦截的请求
	private String filterProcessesUrl = "/qqLogin";
	
	//get set 方法
	//...
}
@ConfigurationProperties(prefix = "blog.security")
public class BlogSecurityProperties {
	
	private QQProperties qqProperties = new QQProperties();

	//get set...
}
@Configuration
//让我们的配置生效
@EnableConfigurationProperties (BlogSecurityProperties.class)
public class BlogSecurityConfig {

}

2、获取QQ用户信息

package com.zzz.blog.social.qq.api;

public interface QQ {

	//返回一个QQ的用户信息
	QQUserInfo getUserInfo();
}
package com.zzz.blog.social.qq.api;

import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;

public class QQImpl extends AbstractOAuth2ApiBinding implements QQ{

	private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
	
	//外界赋值
	private String appId;
	//用户的唯一 标识,url
	private String openId;
	
	private ObjectMapper objectMapper = new ObjectMapper();

	@Override
	public QQUserInfo getUserInfo() {
		// TODO Auto-generated method stub
		return null;
	}

}
package com.zzz.blog.social.qq.api;

public class QQUserInfo {

	private String is_lost;
	private String province;
	private String city;
	private String year;
	private String constellation;
	
	private String ret;
	private String msg;
	private String nickname;
	
	private String figureurl;
	private String figureurl_1;
	private String figureurl_2;
	private String figureurl_qq_1;
	private String figureurl_qq_2;

	private String figureurl_qq;
	private String figureurl_type;
	private String gender_type;
	
	private String gender;
	private String is_yellow_vip;
	private String vip;
	private String yellow_vip_level;
	private String level;
	private String is_yellow_year_vip;
	
	private String openId;
	
	//get/set...
}

修改代码:

	//获取用户信息
	@Override
	public QQUserInfo getUserInfo() {
		
		//拼接参数
		String url = String.format(URL_GET_USERINFO, appId, openId);
		//发送请求
		String result = getRestTemplate().getForObject(url, String.class);
		
		//处理返回值
		QQUserInfo userInfo = null;

			try {
				userInfo = objectMapper.readValue(result, QQUserInfo.class);
				userInfo. setOpenId(openId);
			} catch (JsonProcessingException e) {
				throw new RuntimeException(" 获取用户信息失败!");
			}

		return userInfo;
	}

3、如何获得OpenId以及AppId

在这里插入图片描述

public class QQImpl extends AbstractOAuth2ApiBinding implements QQ{
	
	private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?pauth_consumer_key=%s&openid=%s";
	
	private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
	
	//外界赋值
	private String appId;
	//用户的唯一 标识,url
	private String openId;

	private ObjectMapper objectMapper = new ObjectMapper();

	public QQImpl(String accessToken, String appId) {
		//自动拼接一个参数
		super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER) ;
		//赋值appid 
		this.appId = appId;
		//赋值openid
		//通过url获得openid
		//拼接参数
		String url = String.format(URL_GET_OPENID, accessToken);
		
		//发送请求
		String result = getRestTemplate().getForObject(url, String.class);
		//处理返回值
		//callback( {"client_ id":"100550231", ”openid":"CDF1A28F8698E326D173DE17437FB098"} );
		result = StringUtils.replace(result, "callback( ","");
		result = StringUtils.replace(result, " );","");
		//{"client_ id": "100550231","openid": "CDF1A28F8698E326D173DE17437FB098"}
		OpenId id = null;
		
		try {
			id = objectMapper.readValue(result, OpenId.class);
		} catch (JsonProcessingException e) {
			throw new RuntimeException( "获取OpenId失败! ! ");
		}
		
		//赋值openid
		this.openId = id.getOpenid();
	}
	
	//获取用户信息
	@Override
	public QQUserInfo getUserInfo() {
		...
	}
	
}
package com.zzz.blog.social.qq.api;

public class OpenId {

	private String client_id;
	private String openid;

	//get/set
}

4、完成QQOAuth2Template

package com.zzz.blog.social.qq.template;

import ...

public class QQOAuth2Template extends OAuth2Template{

	public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
		super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
		//使clientId、clientSecret可以拼接到一起
		setUseParametersForClientAuthentication(true);
	}

	//添加text/html
	@Override
	protected RestTemplate createRestTemplate() {
		RestTemplate template = super.createRestTemplate();
		template.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
		
		return template;
	}

	//把请求的格式按照qq的标准,做了一些自定义信息 自己处理请求 按&分割字符,分割后如下
	//access_token=FE04***** *****************CCE2  items[0]
	//expires_in=7776000 item[0]
	//refresh_token=88E4********* **************BE14 item[1]
	@Override
	protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
		
		String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
		
		//StringUtils.split只切割了一次,坑
		String[] items = StringUtils.split(responseStr, "&");
		String[] item = StringUtils.split(items[1], "&");
		
		String access_token =StringUtils.replace(items[0], "access_token=", "");
		Long expires_in = new Long(StringUtils.replace(item[0], "expires_in=", ""));
		String refresh_token = StringUtils.replace(item[1], "refresh_token=", "");
		
		
		return new AccessGrant(access_token, null, refresh_token, expires_in);
	}
	
}

5、完成QQAdapter与ServiceProvider

package com.zzz.blog.social.qq.connection;

import ...

public class QQAdapter implements ApiAdapter<QQ>{

	@Override
	public boolean test(QQ api) {
		// 始终为true
		return true;
	}

	@Override
	public void setConnectionValues(QQ api, ConnectionValues values) {
		//获取userinfo
		QQUserInfo userInfo = api. getUserInfo();
		
		//获取用户名称
		values.setDisplayName(userInfo.getNickname());
		//获取头像
		values.setImageUrl(userInfo.getFigureurl_qq_1());
		//获取个人主页
		values.setProfileUrl(null);
		//openid,用户在服务商中的唯一标识
		values.setProviderUserId(userInfo.getOpenId());
		
	}

	@Override
	public UserProfile fetchUserProfile(QQ api) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void updateStatus(QQ api, String message) {
		// TODO Auto-generated method stub
		
	}

}
package com.zzz.blog.social.qq.connection;

import ...

public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ>{

	//将用户导向认证服务器中的ur1地址,用户在该地址上进行授权
	private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
	//在获取令牌的时候,需要访问的url
	private static final String URL_ACCESSTOEKN = "https://graph.qq.com/oauth2.0/token";
	
	private String appId;
	
	//1-6
	public QQServiceProvider(String appId,String appSecret) {
		super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESSTOEKN));
		this.appId = appId;
	}

	//7-8
	@Override
	public QQ getApi(String accessToken) {
		// TODO Auto-generated method stub
		return new QQImpl(accessToken, appId);
	}
	
}

6、完成QQConfig与ConnectionFactory

package com.zzz.blog.social.qq.connection;

import ...

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ>{

	public QQConnectionFactory(String providerId, String appId,String appSecret) {
		super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
	}

}
package com.zzz.blog.social.qq.config;

import ...

@Configuration
@EnableSocial
@Order(2)
public class QQConfig extends SocialConfigurerAdapter{

	@Autowired
	private BlogSecurityProperties blogSecurityProperties;

	//添加qq创建connection的工厂
	@Override
	public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer,
			Environment environment) {
		
		QQProperties qqConfig = blogSecurityProperties.getQqProperties();

		QQConnectionFactory qqConnectionFactory = new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret());
		
		connectionFactoryConfigurer.addConnectionFactory(qqConnectionFactory);
	}
	
	//获取登陆人
	@Override
	public UserIdSource getUserIdSource() {
		return new AuthenticationNameUserIdSource();
	}
	
}

7、创建表以及创建操作表的类JdbcUsersConnectionRepository

package com.zzz.blog.social.qq.config;

import ...

@Configuration
@EnableSocial
@Order(1)
public class SocialConfig extends SocialConfigurerAdapter{
	
	@Autowired
	private DataSource dataSource;
	
	//登录之后,直接将QQ的数据保存在数据库
	@Override
	public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
		JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
		return repository;
	}	
	
	//改变拦截的请求
	
	//在注册的过程中,拿到了这个SpringSocial中的信息
	//业务完成之后,把用户的id传给了SpringSocial
	
	//打开ConnectController
}

找到socailJDBC表格式,在数据库中执行sql
在这里插入图片描述

8、改变拦截的请求

package com.zzz.blog.social.qq.config;

import ...

public class ZZZSpringSocialConfigurer extends SpringSocialConfigurer{

	private String filterProcessesUrl;

	public ZZZSpringSocialConfigurer(String filterProcessesUrl) {
		this.filterProcessesUrl = filterProcessesUrl;
	}

	
	//将默认的拦截改为qqLogin
	@Override
	protected <T> T postProcess(T object) {
		//获得filter
		SocialAuthenticationFilter filter = (SocialAuthenticationFilter)super.postProcess(object);
		//设置字段
		filter.setFilterProcessesUrl(filterProcessesUrl);
		return (T) filter;
	}
	
}
	//改变拦截的请求 /auth -> /qqLogin
	@Bean
	public SpringSocialConfigurer zzzSocialSecurityConfig() {
		String filterProcessesUrl = blogSecurityProperties.getQqProperties().getFilterProcessesUrl();
		ZZZSpringSocialConfigurer zzzSpringSocialConfigurer = new ZZZSpringSocialConfigurer(filterProcessesUrl);
		return zzzSpringSocialConfigurer;
	}

9、将Social中的配置生效到SpringSecurity中

在SocialConfig类中添加代码

	//在注册的过程中,拿到了这个SpringSocial中的信息
	//业务完成之后,把用户的id传给了SpringSocial
	@Bean
	public ProviderSignInUtils providerSignInUtils() {
		return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator));
	}
	
	//打开ConnectController
	@Bean
	public ConnectController connectController(ConnectionFactoryLocator connectionFactoryLocator,ConnectionRepository connectionRepository) {
		return new ConnectController(connectionFactoryLocator, connectionRepository);
	}

添加apply配置socialconfig

package com.zzz.blog.config;

import ...

//安全配置类
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

	//SpringSecurity加密方法返回值
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Autowired
	private SpringSocialConfigurer zzzSocialSecurityConfig;
	
	//做拦截
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 请求授权
		http.formLogin().and().authorizeRequests()
		//授权放行
		.antMatchers("/*.html").permitAll()
		//所有请求
		.anyRequest()
		//都需要身份认证
		.authenticated().and()
		//43、使用Layer打开select-mood子页面并配置SpringSecurity允许Iframe嵌入页面 
		.headers().frameOptions().disable().and()
		//跨站请求伪造的防护
		.csrf().disable()
		//添加我们所写的spring social配置
		.apply(zzzSocialSecurityConfig);
	}
	
}

10、创建Visitor实体并实现SocialUserDetailsService接口查找Visitor

package com.zzz.blog.domain;

import ...

@Entity
public class Visitor {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String username;
	private String password;
	private String image;
	
	protected Visitor() {
		
	}

	public Visitor(Long id, String username, String password, String image) {
		super();
		this.id = id;
		this.username = username;
		this.password = password;
		this.image = image;
	}

	//get/set
}
package com.zzz.blog.repository;

import ...

public interface VisitorRepository extends CrudRepository<Visitor, Long>{

}
@Component
public class SocialVisitorServiceImpl implements SocialUserDetailsService{

	@Autowired
	private VisitorRepository visitorRepository;
	
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	@Override
	public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {

		//根据userId查找访客
		Optional<Visitor> optional = visitorRepository.findById(new Long(userId));
		Visitor visitor = optional.get();
		if (visitor == null) {
			throw new UsernameNotFoundException(userId);
		}
		
		return new SocialUser(visitor.getUsername(), passwordEncoder.encode(visitor.getPassword()), true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("VISITOR"));
	}

}

11、实现ConnectionSignUp接口添加Visitor

package com.zzz.blog.social.qq.signup;

import ...

@Component
public class DemoConnectionSignUp implements ConnectionSignUp{

	@Autowired
	private VisitorService visitorService;
	
	//根据社交用户的信息,创建一个Visitor并返回唯一标识
	@Override
	public String execute(Connection<?> connection) {
		
		Visitor visitor = new Visitor(null, connection.getDisplayName(), "123456", connection.getImageUrl());
		
		visitor = visitorService.saveVisitory(visitor);
		
		return visitor.getId().toString();
	}

}
package com.zzz.blog.service;

import ...

@Service
public interface VisitorService {

	Visitor saveVisitory(Visitor visitor);

}
package com.zzz.blog.service;

import ...

@Service
public interface VisitorService {

	Visitor saveVisitory(Visitor visitor);

}
package com.zzz.blog.service;

import ...

@Component
public class VisitorServiceImpl implements VisitorService{

	@Autowired
	private VisitorRepository visitorRepository;
	
	@Override
	public Visitor saveVisitory(Visitor visitor) {
		return visitorRepository.save(visitor);
	}

}

SocialConfig添加setConnectionSignUp执行方法

	@Autowired
	private ConnectionSignUp connectionSignUp;
	
	//登录之后,直接将QQ的数据保存在数据库
	@Override
	public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
		JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
		repository.setConnectionSignUp(connectionSignUp);
		return repository;
	}

12、测试QQ登录

application.properties添加代码如下(修改端口):

server.port=80

login.html修改超链接代码如下:

						<a href="/qqLogin/callback.do" class="login100-social-item bg2">
							<i class="fa fa-qq"></i>
						</a>

修改C:\Windows\System32\drivers\etc\hosts文件

127.0.0.1        www.pinzhi365.com

这是别人提供的测试地址。我们也可以到QQ互联官网https://connect.qq.com/上注册用户,创建应用。
在这里插入图片描述
其中回调地址的写法:网站地址/拦截器拦截的路径/服务提供商。
创建完修改QQProperties类上的对应配置即可。
测试通过,控制台打印了添加visitor数据的sql。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/999629.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1688API接入说明(1688商品详情+关键词搜索商品列表)商品详情数据,商品sku数据

1688商家订单详情API接口可以帮助你开拓新的业务机会。通过API&#xff0c;你可以将自己的业务系统与1688平台连接起来&#xff0c;利用1688平台丰富的资源和渠道优势&#xff0c;扩大你的业务范围。此外&#xff0c;1688商家订单详情API接口还支持多种语言和格式&#xff0c;如…

Docker入门,Docker是什么?有什么用?该怎么用?

目录 1. 项目部署时的复杂性&#xff1f; 2. Docker是如何解决依赖兼容问题的&#xff1f; 3. 众多Linux操作系统发行版的区别 4. Docker 是如何实现跨系统运行的&#xff1f; 5. Docker与虚拟机的差别 6. 镜像(Image)与容器(Container) 7. DockerHub 8. Docker 架构 …

Python 操作 CSV

使用过 CSV 文件都知道&#xff1a;如果我们的电脑中装了 WPS 或 Microsoft Office 的话&#xff0c;.csv 文件默认是被 Excel 打开的&#xff0c;那么什么是 CSV 文件&#xff1f;CSV 文件与 Excel 文件有什么区别&#xff1f;如何通过 Python 来操作 CSV 文件呢&#xff1f;带…

创建开机自启的脚本

在启动许多ros节点时有多种方式&#xff0c;我推荐使用launch来启动所有的节点&#xff0c;这也是一种规范的方式。以后会慢慢向这个方向靠。 除此之外还可以通过创建的脚本来启动&#xff1a; 脚本位置不限&#xff0c;只需要&#xff1a; sudo gedit xxx.sh在里面添加相应的…

网站框架识别方法

cms一般有dedecms(织梦&#xff09;&#xff0c;dzcms&#xff0c;phpweb&#xff0c;phpwind&#xff0c;phpcms&#xff0c;ecshop&#xff0c;dvbbs&#xff0c;siteweaver&#xff0c;aspcms&#xff0c;帝国&#xff0c;zblog&#xff0c;wordpress等。 一般cms都有特定的…

ES6——知识点记录

这里写目录标题 1.字符串支持1.codePointAt——根据字符串码元的位置得到其码点2.includes——判断字符串中是否包含指定的子字符串3.startsWith——判断字符串中是否以指定的字符串开始4.endsWith——判断字符串中是否以指定的字符串结尾5.repeat——将字符串重复指定的次数&a…

AB试验(二)统计基础

AB试验&#xff08;二&#xff09;统计基础 随机变量 均值类指标&#xff1a;如用户的平均使用时⻓、平均购买金额、平均购买频率等 概率类指标&#xff1a;如用户点击的概率(点击率)、转化的概率(转化率)、购买的概率 (购买率)等 经验结论&#xff1a;在数量足够大时&#…

Vibro-meter VM600 200-510-041-021数字量控制板卡

Vibro-meter VM600 200-510-041-021 数字量控制模块通常用于振动监测和机械设备的控制系统中&#xff0c;以执行振动数据采集、分析和控制任务。以下是通常情况下数字量控制模块的一些产品特点&#xff1a; 多通道输入&#xff1a;这些模块通常配备多个输入通道&#xff0c;以接…

浅析数字孪生在科学研究中的作用和优势

在科学研究领域&#xff0c;数字孪生技术正迅速崭露头角&#xff0c;为研究人员提供了前所未有的工具和资源&#xff0c;以更深入、更全面地理解复杂的自然现象和系统。本文带大家探讨数字孪生对科学研究的作用&#xff1a; 1. 精准建模和仿真 数字孪生技术允许科学家创建真实…

【Minecraft】Lucky Block(幸运方块)mod介绍

文章目录 mod介绍支持版本mod作者合成方式幸运值使用方法总结 mod介绍 幸运方块模组虽然只是添加了一个方块&#xff0c;两种武器&#xff0c;一种药水&#xff0c;但拥有超过一百种可能性&#xff01; 简单地打破幸运方块&#xff0c;它将开出意想不到的东西&#xff01; 你可…

云原生安全性:构建可信任的云应用的最佳实践

文章目录 云原生安全性的重要性1. 数据隐私2. 恶意攻击3. 合规性要求4. 业务连续性 构建可信任的云应用的最佳实践1. 安全开发2. 身份验证与授权3. 容器安全性4. 监控与审计5. 持续集成与持续交付&#xff08;CI/CD&#xff09;6. 安全培训和教育 未来趋势&#xff1a;服务网格…

Swagger 使用教程

Swagger 官网&#xff1a; API Documentation & Design Tools for Teams | Swagger 整合swagger 依赖&#xff1a; springfox-swagger2 springfox-swagger-ui <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</a…

气象站:从气候预测到环境监测

在我们的日常生活中&#xff0c;气象站的存在可能并不显眼&#xff0c;但实际上&#xff0c;它对我们的生活产生了重要的影响。气象站是一种用于收集和监测天气数据的设施&#xff0c;通过它&#xff0c;我们可以获得各种关于气候和天气的宝贵信息。那么&#xff0c;气象站可以…

Selenium自动化测试框架常见异常分析及解决方法

01 pycharm中导入selenium报错 现象: pycharm中输入from selenium import webdriver, selenium标红 原因1: pycharm使用的虚拟环境中没有安装selenium, 解决方法: 在pycharm中通过设置或terminal面板重新安装selenium 原因2: 当前项目下有selenium.py,和系统包名冲突导致, …

飞行动力学 - 第20节-横向静稳定性 之 基础点摘要

飞行动力学 - 第20节-横向静稳定性 之 基础点摘要 1. 横向静稳定性2. 横向静稳定准则3. 横向静稳定性的组成4. 参考资料 1. 横向静稳定性 2. 横向静稳定准则 对于横向静稳定性飞机&#xff0c;右滚转扰动会产生正侧滑&#xff0c;飞机产生左滚恢复力矩(负)&#xff0c;即 Δ …

Python之数据库(MYSQL)连接

一&#xff09;数据库SQL语言基础 MySQL是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一&#xff0c;在 WEB 应用方面&#xff0c;MySQL是最好的 RDBMS (Relational Database…

想入行视觉设计?先看这篇文章再说!

大家好我是设计师不知名设计师l1m0&#xff0c;今天要给大家分享的设计岗位是&#xff1a;视觉设计师。 在当今数字化时代&#xff0c;视觉设计师扮演着至关重要的角色。他们是创意世界的魔术师&#xff0c;通过各种视觉元素的精妙组合&#xff0c;为品牌、产品和项目赋予生命…

【微信读书】数据内容接口逆向调试02

需求爬取微信读书的某一本书的整本书的内容 增强需求&#xff0c;大批量爬取一批书籍内容 众所周知微信读书是一个很好用的app&#xff0c;他上面书籍的格式很好&#xff0c;质量很高。 本人充值了会员但是看完做完笔记每次还得去翻很不方便&#xff0c;于是想把书籍内容弄下…

专注写作,快速上线:Cpolar+Inis帮助你在Ubuntu上建立博客网站

文章目录 前言1. Inis博客网站搭建1.1. Inis博客网站下载和安装1.2 Inis博客网站测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总…

信息系统项目管理师(第四版)教材精读思维导图-第十一章项目成本管理

请参阅我的另一篇文章&#xff0c;综合介绍软考高项&#xff1a; 信息系统项目管理师&#xff08;软考高项&#xff09;备考总结_计算机技术与软件专业技术_铭记北宸的博客-CSDN博客 本章思维导图PDF格式 本章思维导图XMind源文件 11.1 管理基础 11.2 管理过程 11.3 规划成本管…