要你命三千:老代码中的那些坑

本文分享了在老项目维护过程中遇到的各种问题,包括糟糕的变量命名、硬编码的界面尺寸、混乱的权限管理和委托使用、第三方库的随意修改等问题,并针对这些问题进行了深入的剖析。

转自:http://blog.callmewhy.com/2015/01/20/wtf-in-old-code/

最近在给以前的老项目维护,说起来工作很简单,一个字:改Bug。这看起来平淡无常的工作,实际上凶险无比,藏坑无数。时至今日,感觉整个人都得到了升华。在睡觉前抽空写篇博客,和各位分享一下踩坑经历,一起品味其中的种种酸苦辣 (没甜)。

为保证个码隐私,文中代码均为化名,还望谅解。如有雷同,纯属巧合 (可以通过 git blame 查看是谁写的)。

第一回:变量命名没点数,有时写着还手误

如果要折磨一个强迫症,最好的方法就是用各种恶心的变量名恶心死他。

什么?你说首字母要大写?

@property (nonatomic, assign) PERSONTYPE personType;

什么?你说单词里面要小写?

typedef enum tagPersonType
{
    person_type = 1,
    group_type,
} PERSONTYPE;

什么?你说要用英文单词命名?

- (void)uploadSeccess:(MessageEntity *)message;

什么?你说类前面要加前缀避免冲突?

@interface PMWLogger : NSObject
...
@interface PMTool : NSObject
...
@interface MainControler : NSObject

什么?你说文件要按照目录存放?

- Classes
    - MainControllers
        - MyController
        - Controllers
        - SettingControllers
        - ChatModel.h
        - ChatModel.m
        - SettingControllers (不是手误)
    - Chatting
    - SearchView.h
    - SearchView.m
    - Voice
    - AgentModels
- Public 
    - Common
    - PublicDef.h
    - PublicDef.m

什么?听说OC可以用宏定义?

#define STRHASSBUSTR(str,subStr) ...

各位看官,这,能忍?

正所谓:

命名拼写看心情,文件目录不分明。
随机掺杂宏定义,鸡不安也犬不宁。

第二回:界面全靠神奇数,保准看到就发怵

前阵子在做 iPhone4 和 iPhone6 以及 iPhone6 Plus 的适配工作。

由于历史原因没有用 AutoLayout ,也由于历史原因老代码的布局全是用数字一个一个写死的。这就给适配带来了莫大的困难。

随便拣点代码给大家欣赏欣赏:

UILabel *infoLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 241, 320, 28)];

0 这种数字还好说,241 就完全让人摸不着头脑,至于 320 这个改成屏幕宽度倒也就还好,但是 28 这种神奇数字又是什么呢?

这种代码就是冲着干死队友的不偿命的态度去的。虽然写起来容易,但是维护困难,可读性极差,尤其是有多个控件布局的时候,依赖关系不明显,如果调整布局需要挨个重新计算并设置值,维护起来的酸爽,谁用谁知道。

要说神奇数字,集大成者莫过于此:

CGRect rect = CGRectMake(12.2+(page-1)*320+42.5*(i%7),((totalRows-1)%3)*55+2,42.5,42.5);

那天早上看到这代码差点就抱着键盘委屈的哭了出来。

正所谓:

界面写法各不同,歪门邪道千万种。
有朝一日被辞了,你的代码我不懂。

第三回:私有公有混一处,代理委托亦糊涂

在聊天的时候有这样一个数据类:

@interface HBTalkData : NSObject
{
    UIImage *_firstImage;
    NSArray *_imageArry;
    id _contents;
}
@property (nonatomic, assign) NSInteger messageId;
@property (nonatomic, strong) id contents;
@property (nonatomic, assign) NSTimeInterval timeInterval;
@property (nonatomic) BOOL fromSelf;
@property (nonatomic) BOOL isGroup;
@property (nonatomic, assign) HBTalkDataStatus talkDataStatus;
@property (nonatomic) HBTalkDataContentType contentType;
@property (nonatomic, strong) PersonInfo *personInfo;
@property (nonatomic, strong) UserInfo *cardUser;
@property (nonatomic, assign) CallType callType;
@property (nonatomic, strong) NSString *duartion;
@property (nonatomic, strong) NSString *mPhoneNumber;
@property (nonatomic, strong) NSString *imageList;
@property (nonatomic, strong) NSString *msgDesc;
@property (nonatomic, readonly) UIImage *firstImage;
@property (nonatomic, readonly) NSArray *imageArry;
@property (nonatomic, assign) float     cellHeight;
@property (nonatomic, assign) CGSize    textSize;
@property (nonatomic) NSTimeInterval voiceDuration;
@property (nonatomic) CGFloat dataSize;
@property (nonatomic) NSUInteger bubbleCount;
@property (nonatomic, copy) NSString *chatUserName;
@property (nonatomic, strong) MessageEntity *originalMessage;
@property (nonatomic, strong) HBTalkDataRegisterInfo *registerInfo;

-(void)reset;
- (NSString *)bubbleDescription;
...
@end

纤弱的头文件里塞满了各种属性定义和方法定义,仿佛可以听到头文件的不满和娇喘。

给大家出个题:看下下面的内容,猜一下这个类的文件名是什么:

... // 此处省略20行

@interface PersonInfo : NSObject
... // 此处省略20行
@property (nonatomic, assign)BOOL     isGrey;
@property (nonatomic, assign)BOOL     isBlack;
@property (nonatomic, assign)BOOL     isTop;
@property (nonatomic, assign)BOOL     isStar;

- (BOOL)isStranger;
- (BOOL)isIndividual;
- (BOOL)isDuDuSecretary;

@end

@interface UserInfo : PersonInfo
... // 此处再省20行
@property (nonatomic, assign)BOOL     mobileVerified;
@property (nonatomic, strong)NSString *countryCode;
@property (nonatomic, readonly)NSString *dialogName;
@end

@interface GroupInfo : PersonInfo
... // 此处又省20行
@property (nonatomic, strong)NSString *creater;
@property (nonatomic, assign)int      memberCount;
@property (nonatomic, strong)NSString *members;
@end

嗯然后这个文件叫做 UserInfo.h ,头文件将近100行。大兄弟,我读书少,你不要骗我。把三个类塞在一个文件里这种行为,除了难为队友,实在是没看出来有什么其他动机可言。

正所谓:

头文件里地方小,塞到一处并不好。
外部对象都知道,安全问题可不小。

第四回:消息通知满天飞,委托方法一大堆

我一直在想,到底是什么,让这个项目的开发人员对 NSNotificationCenter 如此痴迷,痴迷的令人陶醉。

在通过 Model 调用业务逻辑的时候,它这样发了一条命令:

// 喂,LOGIN_MODEL,帮我查下有没有更新
[LOGIN_MODEL versionCheckFromAbout:YES];

这个业务是用 GCD 开了新线程来做的,在后台检查有没有更新,如果有更新那么版本号后面会加个感叹号。那么问题来了:你咋告诉我你检查的结果是有更新还是没更新呐?难道要写个委托?然后定义个方法?然后更新的时候指认委托?然后有了结果再告诉委托?听起来就很烦躁嘛那干脆就用通知好了:

if (self.versionStatus != VersionStatusNormal) {
    [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFY_HAS_NEW_VERSION object:nil];
}

然后在需要做处理的类里面添加 Observer 就可以了:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(myIconShouldChange) name:NOTIFY_HAS_NEW_VERSION object:nil];

哈哈哈哈搞定了。

哈哈哈哈你个头啊!整个项目里类似于这种的通知就有十来个,这还是有宏定义的,好追杀一点。对于那些没有宏定义的,随手一写复制粘贴的,不知道还要填坑多少。

通知虽好,但也不要贪杯啊。

看起来轻松,只是 post 了一下就搞定了,但是在 Debug 的时候有点麻烦。尤其是如果有多个 Observer ,改动的时候牵一发而动全身。如果真的是有这样使用的必要倒也罢了,但是本来一个 block 或者 delegate 就能简单清晰的解决,现在却被搞得这么繁重,实在是没有必要。

而且 NSNotificationCenter 的代码基本是一种变相的复制粘贴,十分的不工整。这是个人恩怨了,撇开不提。

NSNotificationCenter 这种只是不痛不痒的小问题,仅仅是逻辑不够优雅,关系不够清晰罢了。但是如果委托使用不当那是恶心的不行。看下这个聊天页面:

@interface ChattingViewController () <UITableViewDataSource, UITableViewDelegate, UITextViewDelegate, ChattingActionsPanelDelegate, ChatModelDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, HBTalkTableViewCellDelegate, EGORefreshTableHeaderDelegate, XTImagePickerControllerDelegate, ChattingInputPanelDelegate, VoiceRecordingButtonTrashBinViewContainer, ChattingUserDetailPanelDelegate, VoiceRecordingButtonDelegate>

这是一个 真实的故事 。整个类将近3000行,有2000多行是委托里定义的方法,你能信?

在这三千行代码里漫步,万事都要小心。因为你不知道 callIn 这种方法到底是定义的私有方法,还是在委托里定义的方法。#pragma mark 自然也是看心情加的,说不定加错了你也不要当真。

有时候委托都删了不见影子了,但是委托里的各种方法还留在以前的类里。

没人敢动。

How to play.

正所谓:

异步回调用通知,委托多的令人痴。
反正老子看不懂,不写代码光写诗。

第五回:第三方库无出处,随手改动无备注

相信做 iOS 的都知道 AFNetworking 这个网络库,在我们的项目里 AFNetworking 分两种,一个是别人家的 AFNetworking ,一个是咱们的 AFNetworking。对奏是这么任性。在一个300行的头文件里,在99行这样低调的位置里,静静的插上了自己的方法,还在上面认认真真的写上了准确的注释:

/*扩展*/
-(void)setDDCImageWithURL:(NSURL *)url
         placeholderImage:(UIImage *)placeholderImage
                  success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
                  failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;

扩展个头啊!你加在人家的头文件里你说你是扩展,谁信?

这种改动遍地都是,特点是极其低调,难以察觉,甚至 TTTAttributedLabel 这种 UI 库也不能避免:改了 init 为了统一字体和颜色。。。

你说这代码,谁敢改?

我还曾经单纯的想给项目加上 Cocoapods 更新一下第三方库,现在想想,Naive。等以后写到新的独立模块的时候再说吧。

正所谓:

项目勤用三方库,随意穿插改无数。
即使类库有更新,试问代码谁维护。

第六回:单个对象多职责,悲伤逆向流成河

在聊天模块有这样一个类:ChatModel ,简直就是个多面手。

上能和服务器聊天,上传聊天消息同步聊天记录:

- (void)reSendMessages;
- (void)receiveSecretaryMessage:(MessageEntity *)msgEntity;
- (void)deleteMessagesByUserInfo:(UserInfo *)user;
- (void)setAudioMessageBePlayed:(AudioMessageEntity *)audioMessage;
- (void)sendBubbleReplyWithCallMessage:(CallMessageEntity *)callMessage;
- (int)saveMessage:(MessageEntity *)message;

下能做本地缓存管理,增删改查样样精通:

- (void)saveCacheMsg:(NSString *)msg UserMd5:(NSString *)md5;
- (NSString *)loadCacheMsgWithMd5:(NSString *)md5;
- (void)clearCacheMsgWithMd5:(NSString *)md5;

至于什么弹窗提醒,上传进度,完成提示,亦是轻松拿下。

以至于你改着改着不知不觉都会走到这里,因为它处理了太多太多的业务逻辑,每次 DEBUG 追杀断点回到这里,都像是一场久别重逢时的相遇,似曾相识。

正所谓:

一人做事一人当,切忌都往类里装。
开发人员干的爽,维护人员很受伤。

第七回:产品突增新功能,一行代码变大神

有时候需求来也匆匆去也匆匆,让人猝不及防。比如一个简单的登录逻辑:

@interface LoginModel ()
@property (nonatomic, strong)NSString *tcpURL;
@property (nonatomic, strong)UserInfo *offlineCallUser;
@property (nonatomic, assign)VersionStatus versionStatus;
@end

突然发现需要在版本更新的时候多个 API 检查,简单,加个 BOOL ,需要的时候设置成 YES 就行:

@property (nonatomic, assign)BOOL isShowVersionUpdate;

但是这个功能在 About 页面又有点改动,简单,再加个 BOOL 就行:

@property (nonatomic, assign)BOOL checkVersionFromAbout;

然后如果已经显示了注册页面又要少一些请求,行,那再加个 BOOL 值:

@property (nonatomic, assign)BOOL isRegisterShow;

得了,这代码只有你能懂了:

@interface LoginModel ()
@property (nonatomic, strong)NSString *tcpURL;
@property (nonatomic, strong)UserInfo *offlineCallUser;
@property (nonatomic, assign)VersionStatus versionStatus;
@property (nonatomic, assign)BOOL isShowVersionUpdate;
@property (nonatomic, assign)BOOL checkVersionFromAbout;
@property (nonatomic, assign)BOOL isRegisterShow;
@end

想象一下实现方法里各种对 BOOL 标记的特殊处理,想象一下 N 个 if 嵌套的壮观场景。

心塞。

正所谓:

凡是都要听产品,各种业务催的紧。
天塌下来也别怕,逻辑清晰自然挺。

第八回:来了任务有委托,多写一行都嫌多

所谓悲哀就是,当程序员发现一个 delegate 就能访问上级的对象,于是便把各种需要通知上级的事情都放在了委托方法里,尽管这些事情与委托本身无关,但是为了实现功能已经不在意这些所谓的设计与美观。

一个简单的 @optional ,甚至可以用同一个 @protocol 获取到各种不同的上级对象,只需要每次调用的时候加个 respondsToSelector就行了。写上十几个可选方法,取一个通俗的委托名,比如 MyDelegate ,然后如果你持有了我但是我还想调用你的方法, so easy ,把你的方法扔到 MyDelegate 即可。

此时的代码便已经不再是一件艺术品,而只是一个平凡普通、毫无生机的花瓶了。

小结

原本还是挺欢快的吐槽,突然就不想写了。

看着以前的人写的代码,不禁有些凄凉。

在项目里用尽了各种低级下流的手段,只为了实现自己的业务。

这是对艺术的侮辱。

<think> 首先,用户的问题是基于之前的引用内容,但引用内容是关于MasterGo MCP协议的,以及一些提到Trae的信息。用户的问题是:“我想了解如何在trae上基于mastergo设计文件实现代码生成 请问在trae上基于mastergo设计文件实现代码生成的方法” 从引用中: - 引用[2]提到:“Trae:更快、更好、更准确 用 Trae 三天开发效率APP-惜命” 和 “复杂页面生成效果还不是很理想,目前还是在 html 页面生成,随后有时间会尝试一下 .vue 文件生成。” - 引用[3]到[5]讨论MasterGo MCP如何实现从设计到代码的转换。 用户明确问的是在Trae上基于MasterGo设计文件实现代码生成的方法。 系统级指令要求: - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 所有数学表达式使用$...$或$$...$$格式(但在这个主题中,可能没有数学表达式)。 - 结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠(基于提供的引用,因为引用是给定的)。 关键点: - MasterGo MCP是一个协议,用于设计工具和开发环境的协作。 - Trae 似乎是另一个工具或框架,可能用于代码生成,但引用[2]提到它目前主要生成HTML,计划尝试Vue文件。 - 用户想结合两者:基于MasterGo的设计文件,在Trae上实现代码生成。 步骤: 1. **理解整合点**:MasterGo生成设计文件(如通过MCP协议提供结构化数据),Trae可能是一个代码生成工具。需要将MasterGo的设计数据传输给Trae进行代码生成。 2. **基于引用信息**: - 引用[4]和[5]提供了一些代码示例:如使用MCP监控和设计到代码的逻辑。 - 引用[3]和[5]讨论了设计到代码的转换。 - 引用[2]提到Trae在生成HTML页面,计划扩展到.vue文件。 3. **方法步骤**: - 首先,确保MasterGo设计文件通过MCP协议暴露数据。 - 然后,Trae需要能够接收和处理这些数据来生成代码。 - 由于引用中没有直接整合Trae和MasterGo的详细信息,需要基于一般逻辑推断。 - 参考伪代码在引用[5]:设计到代码的函数,如`design_to_code(design_url)`,其中使用MCP获取数据。 假设整合方法: - **获取设计数据**:使用MasterGo MCP API获取设计稿的结构化数据(如布局树、设计变量、组件语义)。 - **Trae处理**:Trae可能是一个代码生成库或工具;需要配置Trae来使用这些数据生成代码。 - **实现步骤**: 1. 设置MasterGo集成:确保设计文件在MasterGo中准备好,并通过MCP暴露。 2. 使用MCP获取数据:使用JavaScript或其他语言调用MCP API获取DSL数据。 3. 集成Trae:将获取的数据传递给Trae的代码生成功能。 4. 生成代码:Trae基于数据生成HTML或其他代码格式。 引用支持: - 引用[5]:"设计稿到代码的AI革命",有伪代码:`def design_to_code(design_url): dsl_data = mcp_getDSL(design_url); ...` - 引用[4]:有JavaScript代码示例使用MCPHealthChecker。 - 引用[2]:Trae生成HTML页面。 因此,方法可能涉及: - 在Trae中实现一个适配器,使用MCP API获取数据。 - 由于Trae当前支持HTML生成,它可能输出HTML代码;如果需要Vue,可能需要扩展。 回答结构: 1. **引言**:解释整合的必要性。 2. **步骤**:一步一步描述如何实现。 3. **代码示例**:如果可能,提供伪代码代码片段(基于引用)。 4. **注意事项**:提到当前的局限性,如Trae只生成HTML。 5. **相关问题**:生成相关问题。 引用标识: - 在引用段落末尾添加[^数字],数字对应引用索引(1到5)。 确保回答真实可靠:基于提供的引用;不要编造未提供的信息。 最终回答大纲: - 介绍:如何在Trae上基于MasterGo设计文件实现代码生成。 - 详细方法步骤: a. 准备工作:确保MasterGo文件和MCP设置。 b. 获取设计数据:使用MCP API。 c. 配置Trae:传递数据给Trae生成代码。 d. 生成输出。 - 代码示例:基于引用伪代码。 - 注意事项:如Trae的当前限制。 - 生成相关问题。 </think> 基于您的查询和提供的引用内容,我将详细介绍如何在Trae上基于MasterGo设计文件实现代码生成的方法。MasterGo MCP(Multi-Platform Collaboration Protocol)是一个专用于设计-开发协作的协议,它能将设计稿(如布局、组件和变量)转换为结构化数据,而Trae是一个开发效率工具,专注于从设计数据生成前端代码(目前主要支持HTML,计划扩展到Vue文件)。整合两者需要利用MCP协议的API来获取设计数据,并将其输入Trae的代码生成引擎。 以下是逐步实现方法,基于引用中的核心功能和技术细节(如引用[4]和[5]的代码示例)。整个过程分为准备工作、数据获取、Trae集成和代码生成四个阶段,确保真实可靠。 ### 方法步骤 1. **准备工作:设置MasterGo设计文件和MCP环境** 首先,在MasterGo中准备您的设计文件(如Figma或Sketch稿件),并确保启用MCP协议以暴露结构化数据。MCP支持实时同步设计元素,包括布局树、组件语义和设计变量(如颜色、间距)。这一步需在MasterGo工具中完成: - 导出一键共享链接或URL(引用[5]提到的设计稿URL)。 - 配置MCP端点,例如在本地或云端设置WebSocket服务(引用[4]的JavaScript示例中使用了`ws://localhost:8080/mcp`)。 > 引用[1]强调MCP的核心功能是“支持实时同步设计稿、组件库及开发参数”[^1],这为后续数据交互奠定基础。 2. **获取设计数据:通过MCP API提取结构化DSL** 使用编程语言(如JavaScript或Python)调用MCP API,获取设计稿的Domain-Specific Language(DSL)数据。DSL包含布局信息、组件类型和设计变量,可直接用于代码生成。 - 参考引用[5]的伪代码逻辑:定义一个函数来获取和处理数据。 ```python # 伪代码示例:基于MCP获取设计数据 def fetch_design_data(design_url): # 使用MCP API调用获取DSL数据 dsl_data = mcp_getDSL(design_url) # mcp_getDSL是MCP提供的客户端方法 return dsl_data # 返回结构化数据,如布局树和组件映射 ``` - 在实际应用中,您可能需要安装MCP的客户端库(如`@mastergo/mcp-monitor`),并处理性能监控(引用[4]的`MCPHealthChecker`示例可确保连接稳定)[^4]。 3. **集成Trae:配置Trae接收和处理设计数据** Trae作为一个代码生成工具,需要适配输入数据格式(目前Trae主要生成HTML代码,但引用[2]提到正在探索Vue文件生成)。将MCP获取的DSL数据传递给Trae: - 创建一个Trae适配器函数,将DSL数据转换为Trae可识别的格式(如JSON对象)。 - 由于Trae专注于快速开发(引用[2]称其“更快、更好、更准确”),您可以直接在JavaScript中调用Trae的生成函数。 ```javascript // JavaScript示例:传递数据到Trae生成代码 const trae = require('trae-codegen'); // 假设Trae提供Node.js库 function generate_with_trae(dsl_data) { // 映射DSL数据到Trae输入格式 const trae_input = { layout: dsl_data.layout_tree, variables: dsl_data.design_variables, components: dsl_data.component_semantics }; // 调用Trae生成代码(当前Trae输出HTML) const html_code = trae.generate(trae_input, { format: 'html' }); // 选项指定输出格式 return html_code; } ``` > 引用[2]指出“复杂页面生成效果还不是很理想”,建议从简化设计开始测试[^2]。 4. **生成和输出代码:在Trae中执行生成逻辑** 结合以上步骤,编写一个主函数来处理整个流程:从设计稿URL获取数据,通过Trae生成代码,并保存或输出结果。 - 完整伪代码示例(使用Python或Node.js): ```python # 主函数示例(Python伪代码) import requests # 用于HTTP调用MCP API from trae_codegen import generate as trae_generate # Trae的生成模块 def design_to_code_in_trae(design_url): # 步骤1:获取MCP数据 dsl_data = requests.get(f"{design_url}/mcp/dsl").json() # 假设MCP提供RESTful端点 # 步骤2:Trae生成代码 html_output = trae_generate(dsl_data) # 可选:保存到文件或输出到控制台 with open('output.html', 'w') as file: file.write(html_output) return html_output # 使用示例 design_url = "https://design.mastergo.com/my-project" # MasterGo设计稿URL generated_code = design_to_code_in_trae(design_url) ``` - 注意事项: - **性能优化**:引用[4]建议监控MCP连接(如使用`MCPHealthChecker`),避免超时问题[^4]。 - **格式限制**:当前Trae仅支持HTML输出(引用[2]提到“html 页面生成”),如需Vue组件,可扩展Trae的映射逻辑或等待官方更新。 - **上下文感知**:引用[5]强调“组件语义映射”是关键,确保Trae能正确识别按钮、表单等元素[^5]。 ### 潜在挑战与避建议 - **数据兼容性问题**:MCP的DSL数据可能与Trae输入格式不匹配。解决方案:在Trae适配器中添加转换层,例如将设计变量(如间距值)归一化为CSS友好单位。 - **生成效果限制**:对于复杂页面(如动态交互),Trae的生成可能不理想(引用[2])。建议从简单组件开始测试,逐步迭代。 - **工具依赖**:确保安装必要的库(如MasterGo的MCP SDK和Trae的代码包),引用[4]提供了监控模块示例[^4]。 通过以上方法,您可以在Trae上实现基于MasterGo设计文件的代码生成。该方法基于MCP协议的实时数据同步和Trae的高效生成能力,能有效提升设计开发协作效率(引用[3]称其“彻底改变低效协作模式”)[^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值