Control Structures

本文详细介绍了C++中的控制结构,包括条件结构(if和else)、迭代结构(while、do-while和for循环),以及跳转语句(break、continue和goto)。通过具体的示例代码展示了如何使用这些控制结构来实现复杂的逻辑流程。

Control Structures

A program is usually not limited to a linear sequence of instructions. During its process it may bifurcate, repeat code or take decisions. For that purpose, C++ provides control structures that serve to specify what has to be done by our program, when and under which circumstances.

With the introduction of control structures we are going to have to introduce a new concept: the compound-statement or block. A block is a group of statements which are separated by semicolons (;) like all C++ statements, but grouped together in a block enclosed in braces: { }:

{ statement1; statement2; statement3; }

Most of the control structures that we will see in this section require a generic statement as part of its syntax. A statement can be either a simple statement (a simple instruction ending with a semicolon) or a compound statement (several instructions grouped in a block), like the one just described. In the case that we want the statement to be a simple statement, we do not need to enclose it in braces ( {}). But in the case that we want the statement to be a compound statement it must be enclosed between braces ( {}), forming a block.

Conditional structure: if and else

The if keyword is used to execute a statement or block only if a condition is fulfilled. Its form is:

if (condition) statement

Where condition is the expression that is being evaluated. If this condition is true, statement is executed. If it is false, statement is ignored (not executed) and the program continues right after this conditional structure.
For example, the following code fragment prints x is 100 only if the value stored in the x variable is indeed 100:

1
2
if (x == 100)
  cout << "x is 100";


If we want more than a single statement to be executed in case that the condition is true we can specify a block using braces { }:

1
2
3
4
5
if (x == 100)
{
   cout << "x is ";
   cout << x;
}


We can additionally specify what we want to happen if the condition is not fulfilled by using the keyword else. Its form used in conjunction with if is:

if (condition) statement1 else statement2

For example:

1
2
3
4
if (x == 100)
  cout << "x is 100";
else
  cout << "x is not 100";


prints on the screen x is 100 if indeed x has a value of 100, but if it has not -and only if not- it prints out x is not 100.

The if + else structures can be concatenated with the intention of verifying a range of values. The following example shows its use telling if the value currently stored in x is positive, negative or none of them (i.e. zero):

1
2
3
4
5
6
if (x > 0)
  cout << "x is positive";
else if (x < 0)
  cout << "x is negative";
else
  cout << "x is 0";


Remember that in case that we want more than a single statement to be executed, we must group them in a block by enclosing them in braces { }.

Iteration structures (loops)


Loops have as purpose to repeat a statement a certain number of times or while a condition is fulfilled.

The while loop
Its format is:

while (expression) statement

and its functionality is simply to repeat statement while the condition set in expression is true.
For example, we are going to make a program to countdown using a while-loop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// custom countdown using while

#include <iostream>
using namespace std;

int main ()
{
  int n;
  cout << "Enter the starting number > ";
  cin >> n;

  while (n>0) {
    cout << n << ", ";
    --n;
  }

  cout << "FIRE!\n";
  return 0;
}
Enter the starting number > 8
8, 7, 6, 5, 4, 3, 2, 1, FIRE!


When the program starts the user is prompted to insert a starting number for the countdown. Then the while loop begins, if the value entered by the user fulfills the condition n>0 (that n is greater than zero) the block that follows the condition will be executed and repeated while the condition ( n>0) remains being true.

The whole process of the previous program can be interpreted according to the following script (beginning in main):

  1. User assigns a value to n
  2. The while condition is checked (n>0). At this point there are two possibilities:
    * condition is true: statement is executed (to step 3)
    * condition is false: ignore statement and continue after it (to step 5)
  3. Execute statement:
    cout << n << ", ";
    --n;
    (prints the value of n on the screen and decreases n by 1)
  4. End of block. Return automatically to step 2
  5. Continue the program right after the block: print FIRE! and end program.

When creating a while-loop, we must always consider that it has to end at some point, therefore we must provide within the block some method to force the condition to become false at some point, otherwise the loop will continue looping forever. In this case we have included --n; that decreases the value of the variable that is being evaluated in the condition ( n) by one - this will eventually make the condition ( n>0) to become false after a certain number of loop iterations: to be more specific, when n becomes 0, that is where our while-loop and our countdown end.

Of course this is such a simple action for our computer that the whole countdown is performed instantly without any practical delay between numbers.

The do-while loop

Its format is:

do statement while (condition);

Its functionality is exactly the same as the while loop, except that condition in the do-while loop is evaluated after the execution of statement instead of before, granting at least one execution of statement even if condition is never fulfilled. For example, the following example program echoes any number you enter until you enter 0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// number echoer

#include <iostream>
using namespace std;

int main ()
{
  unsigned long n;
  do {
    cout << "Enter number (0 to end): ";
    cin >> n;
    cout << "You entered: " << n << "\n";
  } while (n != 0);
  return 0;
}
Enter number (0 to end): 12345
You entered: 12345
Enter number (0 to end): 160277
You entered: 160277
Enter number (0 to end): 0
You entered: 0


The do-while loop is usually used when the condition that has to determine the end of the loop is determined within the loop statement itself, like in the previous case, where the user input within the block is what is used to determine if the loop has to end. In fact if you never enter the value 0 in the previous example you can be prompted for more numbers forever.

The for loop

Its format is:

for (initialization; condition; increase) statement;

and its main function is to repeat statement while condition remains true, like the while loop. But in addition, the for loop provides specific locations to contain an initialization statement and an increase statement. So this loop is specially designed to perform a repetitive action with a counter which is initialized and increased on each iteration.

It works in the following way:

  1. initialization is executed. Generally it is an initial value setting for a counter variable. This is executed only once.
  2. condition is checked. If it is true the loop continues, otherwise the loop ends and statement is skipped (not executed).
  3. statement is executed. As usual, it can be either a single statement or a block enclosed in braces { }.
  4. finally, whatever is specified in the increase field is executed and the loop gets back to step 2.

Here is an example of countdown using a for loop:

1
2
3
4
5
6
7
8
9
10
11
// countdown using a for loop
#include <iostream>
using namespace std;
int main ()
{
  for (int n=10; n>0; n--) {
    cout << n << ", ";
  }
  cout << "FIRE!\n";
  return 0;
}
10, 9, 8, 7, 6, 5, 4, 3, 2, 1, FIRE!


The initialization and increase fields are optional. They can remain empty, but in all cases the semicolon signs between them must be written. For example we could write: for (;n<10;) if we wanted to specify no initialization and no increase; or for (;n<10;n++) if we wanted to include an increase field but no initialization (maybe because the variable was already initialized before).

Optionally, using the comma operator ( ,) we can specify more than one expression in any of the fields included in a for loop, like in initialization, for example. The comma operator ( ,) is an expression separator, it serves to separate more than one expression where only one is generally expected. For example, suppose that we wanted to initialize more than one variable in our loop:

1
2
3
4
for ( n=0, i=100 ; n!=i ; n++, i-- )
{
   // whatever here...
}


This loop will execute for 50 times if neither n or i are modified within the loop:


n starts with a value of 0, and i with 100, the condition is n!=i (that n is not equal to i). Because n is increased by one and i decreased by one, the loop's condition will become false after the 50th loop, when both n and i will be equal to 50.

Jump statements.


The break statement

Using break we can leave a loop even if the condition for its end is not fulfilled. It can be used to end an infinite loop, or to force it to end before its natural end. For example, we are going to stop the count down before its natural end (maybe because of an engine check failure?):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// break loop example

#include <iostream>
using namespace std;

int main ()
{
  int n;
  for (n=10; n>0; n--)
  {
    cout << n << ", ";
    if (n==3)
    {
      cout << "countdown aborted!";
      break;
    }
  }
  return 0;
}
10, 9, 8, 7, 6, 5, 4, 3, countdown aborted!


The continue statement

The continue statement causes the program to skip the rest of the loop in the current iteration as if the end of the statement block had been reached, causing it to jump to the start of the following iteration. For example, we are going to skip the number 5 in our countdown:

1
2
3
4
5
6
7
8
9
10
11
12
13
// continue loop example
#include <iostream>
using namespace std;

int main ()
{
  for (int n=10; n>0; n--) {
    if (n==5) continue;
    cout << n << ", ";
  }
  cout << "FIRE!\n";
  return 0;
}
10, 9, 8, 7, 6, 4, 3, 2, 1, FIRE!


The goto statement
goto allows to make an absolute jump to another point in the program. You should use this feature with caution since its execution causes an unconditional jump ignoring any type of nesting limitations.
The destination point is identified by a label, which is then used as an argument for the goto statement. A label is made of a valid identifier followed by a colon ( :).

Generally speaking, this instruction has no concrete use in structured or object oriented programming aside from those that low-level programming fans may find for it. For example, here is our countdown loop using goto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// goto loop example

#include <iostream>
using namespace std;

int main ()
{
  int n=10;
  loop:
  cout << n << ", ";
  n--;
  if (n>0) goto loop;
  cout << "FIRE!\n";
  return 0;
}
10, 9, 8, 7, 6, 5, 4, 3, 2, 1, FIRE!


The exit function

exit is a function defined in the cstdlib library.

The purpose of exit is to terminate the current program with a specific exit code. Its prototype is:

void exit (int exitcode);


The exitcode is used by some operating systems and may be used by calling programs. By convention, an exit code of 0 means that the program finished normally and any other value means that some error or unexpected results happened.

The selective structure: switch.

The syntax of the switch statement is a bit peculiar. Its objective is to check several possible constant values for an expression. Something similar to what we did at the beginning of this section with the concatenation of several if and else if instructions. Its form is the following:

switch (expression)
{
  case constant1:
     group of statements 1;
     break;
  case constant2:
     group of statements 2;
     break;
  .
  .
  .
  default:
     default group of statements
}

It works in the following way: switch evaluates expression and checks if it is equivalent to constant1, if it is, it executes group of statements 1 until it finds the break statement. When it finds this break statement the program jumps to the end of the switch selective structure.

If expression was not equal to constant1 it will be checked against constant2. If it is equal to this, it will execute group of statements 2 until a break keyword is found, and then will jump to the end of the switch selective structure.

Finally, if the value of expression did not match any of the previously specified constants (you can include as many case labels as values you want to check), the program will execute the statements included after the default: label, if it exists (since it is optional).

Both of the following code fragments have the same behavior:

switch exampleif-else equivalent
switch (x) {
  case 1:
    cout << "x is 1";
    break;
  case 2:
    cout << "x is 2";
    break;
  default:
    cout << "value of x unknown";
  }
if (x == 1) {
  cout << "x is 1";
  }
else if (x == 2) {
  cout << "x is 2";
  }
else {
  cout << "value of x unknown";
  }

The switch statement is a bit peculiar within the C++ language because it uses labels instead of blocks. This forces us to put break statements after the group of statements that we want to be executed for a specific condition. Otherwise the remainder statements -including those corresponding to other labels- will also be executed until the end of the switch selective block or a break statement is reached.

For example, if we did not include a break statement after the first group for case one, the program will not automatically jump to the end of the switch selective block and it would continue executing the rest of statements until it reaches either a break instruction or the end of the switch selective block. This makes it unnecessary to include braces { } surrounding the statements for each of the cases, and it can also be useful to execute the same block of instructions for different possible values for the expression being evaluated. For example:

1
2
3
4
5
6
7
8
9
switch (x) {
  case 1:
  case 2:
  case 3:
    cout << "x is 1, 2 or 3";
    break;
  default:
    cout << "x is not 1, 2 nor 3";
  }


Notice that switch can only be used to compare an expression against constants. Therefore we cannot put variables as labels (for example case n: where n is a variable) or ranges ( case (1..3):) because they are not valid C++ constants.

If you need to check ranges or values that are not constants, use a concatenation of if and else if statements.
import os import re import csv from openpyxl import load_workbook, Workbook import difflib import sys import io import time import shutil from pathlib import Path from collections import defaultdict import traceback # 设置系统标准输出为UTF-8 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') def recursive_compare_dirs(old_dir, new_dir): """ 递归比较两个目录,获取变更文件和变更行号(基于新文件行号) """ changed_files = defaultdict(set) # 使用更高效的os.scandir替代os.walk for entry in os.scandir(new_dir): if entry.is_dir(): # 递归处理子目录 subdir_path = os.path.join(old_dir, entry.name) if os.path.exists(subdir_path): sub_changed = recursive_compare_dirs( subdir_path, os.path.join(new_dir, entry.name) ) for rel_path, lines in sub_changed.items(): changed_files[os.path.join(entry.name, rel_path)] = lines elif entry.is_file(): rel_path = os.path.relpath(entry.path, new_dir) old_path = os.path.join(old_dir, rel_path) # 处理新增文件 if not os.path.exists(old_path): try: # 使用更高效的行数统计方法 with open(entry.path, 'rb') as f: line_count = sum(1 for _ in f) # 标记所有行为已变更(基于新文件行号) changed_lines = set(range(1, line_count + 1)) changed_files[rel_path] = changed_lines print(f"新增文件: {rel_path}, 行数: {line_count}") except Exception as e: print(f"读取新文件出错: {entry.path} - {e}") continue # 处理修改文件 - 使用文件大小和修改时间快速过滤 if (os.path.getsize(entry.path) == os.path.getsize(old_path) and os.path.getmtime(entry.path) <= os.path.getmtime(old_path)): continue try: # 读取文件内容 with open(old_path, 'r', encoding='utf-8', errors='ignore') as f_old: old_content = f_old.readlines() with open(entry.path, 'r', encoding='utf-8', errors='ignore') as f_new: new_content = f_new.readlines() # 比较内容差异并获取变更行号(基于新文件) changed_lines = detect_changed_lines(old_content, new_content) if changed_lines: changed_files[rel_path] = changed_lines print(f"变更文件: {rel_path}, 变更行数: {len(changed_lines)}") except Exception as e: print(f"比较文件出错: {rel_path} - {e}") return dict(changed_files) def detect_changed_lines(old_content, new_content): """ 优化版:检测文件中的变更行号(基于新文件行号) """ changed_lines = set() matcher = difflib.SequenceMatcher(None, old_content, new_content) for opcode in matcher.get_opcodes(): if opcode[0] != 'equal': # 获取新文件中的变更行号范围(索引+1转换为实际行号) start = opcode[3] end = opcode[4] # 转换为基于1的行号 changed_lines.update(range(start + 1, end + 1)) return changed_lines def detect_encoding(file_path): """优化版:检测文件编码""" # 常见编码类型列表(优先级排序) encodings = ['utf-8', 'utf-16', 'cp932', 'shift_jis', 'gbk', 'big5', 'latin1'] for encoding in encodings: try: with open(file_path, 'r', encoding=encoding) as f: f.read(4096) return encoding except: continue return 'utf-8' def update_excel_sheets(csv_folder, output_excel, changed_files): """优化版:更新Excel表格""" try: print(f"开始更新Excel: {output_excel}") # 加载或创建Excel文件 if os.path.exists(output_excel): print(f"加载现有Excel文件: {output_excel}") wb = load_workbook(output_excel) print(f"现有工作表: {wb.sheetnames}") else: print("创建新的Excel文件") wb = Workbook() # 删除默认创建的工作表 for sheet_name in wb.sheetnames: wb.remove(wb[sheet_name]) # 创建所需的工作表 wb.create_sheet("ファイル差分") wb.create_sheet("_org_fm") wb.create_sheet("warn") # === 功能1: 写入文件差分表 === print("\n=== 写入文件差分表 ===") if "ファイル差分" not in wb.sheetnames: wb.create_sheet("ファイル差分") print("创建'ファイル差分'工作表") ws_diff = wb["ファイル差分"] # 保留标题行和公式列(只清除A列的数据) if ws_diff.max_row > 1: print(f"清除'ファイル差分'工作表A列数据 (现有行数: {ws_diff.max_row})") # 只删除A列第二行及以后的数据,保留其他列 for row_idx in range(2, ws_diff.max_row + 1): if ws_diff.cell(row=row_idx, column=1).value is not None: ws_diff.cell(row=row_idx, column=1, value=None) # 确保有标题行 if ws_diff["A1"].value != "文件路径": ws_diff["A1"] = "文件路径" # 写入变更文件到A列第二行开始 print(f"写入 {len(changed_files)} 个变更文件路径到A列") row_num = 2 for file_path in changed_files.keys(): ws_diff.cell(row=row_num, column=1, value=file_path) row_num += 1 # === 功能2: 复制func_met.csv到_org_fm工作表 === func_met_path = os.path.join(csv_folder, "func_met.csv") if os.path.exists(func_met_path): print("\n=== 处理func_met.csv ===") if "_org_fm" not in wb.sheetnames: wb.create_sheet("_org_fm") print("创建'_org_fm'工作表") ws_fm = wb["_org_fm"] # 保留标题行在第一行(不清除标题行) # 清除数据行(第二行及以后) if ws_fm.max_row > 1: print(f"清除'_org_fm'工作表数据行 (现有行数: {ws_fm.max_row})") ws_fm.delete_rows(2, ws_fm.max_row - 1) # 读取CSV文件 encoding = detect_encoding(func_met_path) print(f"检测到func_met.csv编码: {encoding}") # 使用csv模块按行读取处理 with open(func_met_path, 'r', encoding=encoding, errors='replace') as f: csv_reader = csv.reader(f) # 跳过标题行(因为Excel中已经有标题行) next(csv_reader) # 写入数据行 print("写入func_met.csv数据行...") row_count = 0 for row in csv_reader: ws_fm.append(row) row_count += 1 if row_count % 1000 == 0: print(f"已写入 {row_count} 行数据...") print(f"写入完成,共 {row_count} 行数据") else: print(f"未找到func_met.csv: {func_met_path}") # === 功能3: 高效处理warn.csv === warn_path = os.path.join(csv_folder, "warn.csv") if os.path.exists(warn_path): print("\n=== 处理warn.csv ===") if "warn" not in wb.sheetnames: wb.create_sheet("warn") print("创建'warn'工作表") ws_warn = wb["warn"] # 写入标题行(第二行) headers = ['Source', 'Line #', 'Level', 'Warn #', 'Message', 'WarnFilter'] # 修改列名 for col_idx, header in enumerate(headers, 1): ws_warn.cell(row=2, column=col_idx, value=header) else: ws_warn = wb["warn"] print(f"使用现有'warn'工作表") # === 关键修复:构建变更文件映射 === file_map = {} for file_path, changed_lines in changed_files.items(): # 文件名映射 filename = Path(file_path).name if filename not in file_map: file_map[filename] = set() file_map[filename] = file_map[filename].union(changed_lines) # 完整路径映射 file_map[file_path] = changed_lines # 准备列名映射(根据标题行确定列位置) header_mapping = {} for col in range(1, ws_warn.max_column + 1): header_value = ws_warn.cell(row=2, column=col).value if header_value: header_mapping[header_value] = col print(f"列名映射: {header_mapping}") # 获取现有的'WarnFilter'列的列号 change_col_idx = header_mapping.get('WarnFilter') if not change_col_idx: # 如果'WarnFilter'列不存在,创建它 change_col_idx = ws_warn.max_column + 1 ws_warn.cell(row=2, column=change_col_idx, value='WarnFilter') header_mapping['WarnFilter'] = change_col_idx print(f"添加'WarnFilter'列到第 {change_col_idx} 列") else: print(f"找到'WarnFilter'列在第 {change_col_idx} 列") # 读取CSV文件 encoding = detect_encoding(warn_path) print(f"检测到warn.csv编码: {encoding}") # 使用csv模块按行处理 with open(warn_path, 'r', encoding=encoding, errors='replace') as f: csv_reader = csv.DictReader(f) fieldnames = csv_reader.fieldnames # 打印CSV字段信息 print(f"CSV字段: {fieldnames}") # CSV列名到Excel列名的映射 column_mapping = { 'File': 'Source', 'Line': 'Line #', 'Grp': 'Level', 'Nbr': 'Warn #', 'Description': 'Message' } # 准备数据 match_count = 0 row_offset = 2 # 标题行位置 # === 关键修改:清空需要覆盖的列 === # 确定需要覆盖的列索引 overwrite_columns = [header_mapping[col] for col in ['Source', 'Line #', 'Level', 'Warn #', 'Message']] # 清空这些列从第三行开始的数据 print(f"清空需要覆盖的列: {overwrite_columns}") for row_idx in range(3, ws_warn.max_row + 1): for col_idx in overwrite_columns: ws_warn.cell(row=row_idx, column=col_idx, value=None) # 重置行计数器 next_row = 3 processed_rows = 0 csv_total_rows = 0 # 处理CSV中的每一行 for csv_row in csv_reader: processed_rows += 1 csv_total_rows += 1 # 创建新行数据 new_row = { header: csv_row.get(csv_col, '') for csv_col, header in column_mapping.items() } # 计算变更标记 source = str(new_row.get('Source', '')) try: line_num = int(new_row.get('Line #', 0)) if new_row.get('Line #') else 0 except (ValueError, TypeError): line_num = 0 # 获取警告编号 warn_num = new_row.get('Warn #', '') change_marker = 'No' if source and line_num > 0: # 尝试文件名匹配 filename = Path(source).name if filename in file_map and line_num in file_map[filename]: change_marker = 'Yes' match_count += 1 # 尝试完整路径匹配 elif source in file_map and line_num in file_map[source]: change_marker = 'Yes' match_count += 1 # 写入数据到特定列(覆盖模式) for col_name, value in new_row.items(): if col_name in header_mapping: col_idx = header_mapping[col_name] ws_warn.cell(row=next_row, column=col_idx, value=value) # 写入变更标记到'WarnFilter'列 ws_warn.cell(row=next_row, column=change_col_idx, value=change_marker) next_row += 1 if processed_rows % 1000 == 0: print(f"已处理 {processed_rows} 行数据...") # 打印统计信息 print(f"CSV总行数: {csv_total_rows}") print(f"匹配到 {match_count} 条变更警告") print(f"总共处理了 {processed_rows} 行数据") # 清理多余行(如果原有行数多于CSV行数) if next_row < ws_warn.max_row: print(f"删除多余的行 ({next_row} 到 {ws_warn.max_row})") ws_warn.delete_rows(next_row, ws_warn.max_row - next_row + 1) # 检查行数是否匹配 ws_total_rows = ws_warn.max_row - 2 # 减去标题行 if ws_total_rows != csv_total_rows: print(f"警告: 工作表行数({ws_total_rows})与CSV行数({csv_total_rows})不匹配!") else: print("工作表行数与CSV行数匹配") else: print(f"未找到warn.csv: {warn_path}") # === 保存Excel文件 === print("\n保存Excel文件...") save_start = time.time() try: wb.save(output_excel) save_duration = time.time() - save_start print(f"Excel保存完成,耗时: {save_duration:.2f}秒") print(f"最终文件: {output_excel}") file_size = os.path.getsize(output_excel) / 1024 / 1024 print(f"文件大小: {file_size:.2f} MB") # 验证保存结果 if os.path.exists(output_excel): print("文件保存验证成功") else: print("!!! 文件保存后不存在,保存可能失败 !!!") return True except Exception as save_error: print(f"保存Excel时出错: {save_error}") return False except Exception as e: print(f"\n!!! 更新Excel出错: {str(e)} !!!") traceback.print_exc() return False def main(): # 配置路径(根据实际情况修改) old_code_dir = r"E:\system\Desktop\项目所需文件\工具\ffff\QAC\code\old\GA_D82DD83D_00-00-07\mainline\spa_traveo\src" new_code_dir = r"E:\system\Desktop\项目所需文件\工具\ffff\QAC\code\new\GA_D82DD83D_00-00-08\mainline\spa_traveo\src" csv_folder = r"E:\system\Desktop\项目所需文件\工具\ffff\QAC\APL\Tool出力結果" output_excel = r"E:\system\Desktop\项目所需文件\工具\ffff\QAC\GA_D24D_00-00-01(三回目)_QAC.xlsx" print("="*80) print("开始文件比较...") print(f"旧代码目录: {old_code_dir}") print(f"新代码目录: {new_code_dir}") print(f"CSV文件夹: {csv_folder}") print(f"输出Excel: {output_excel}") try: start_time = time.time() # 获取变更文件(完全使用递归比较) changed_files = recursive_compare_dirs(old_code_dir, new_code_dir) print(f"\n找到 {len(changed_files)} 个变更文件") # 打印变更文件详情 print("\n变更文件详情:") for i, (file_path, lines) in enumerate(changed_files.items()): if i < 10: # 最多打印10个文件详情 print(f"{i+1}. {file_path}: 变更行数 {len(lines)}") if len(lines) < 10: # 打印少于10行的变更行号 print(f" 行号: {sorted(lines)}") # 更新Excel print("="*80) print("更新Excel表格...") success = update_excel_sheets(csv_folder, output_excel, changed_files) total_time = time.time() - start_time print(f"\n总处理时间: {total_time:.2f}秒") if success: print(f"\n处理完成! 输出文件: {output_excel}") else: print("\n处理失败,请检查错误日志") except Exception as e: print(f"\n!!! 处理过程中发生严重错误: {str(e)} !!!") traceback.print_exc() print("="*80) print("程序结束") if __name__ == "__main__": main() 上述代码只适合在作成QAC成果物时适用,下面我想在这个代码里面新增做成CodeSonar成果物的功能,与QAC的差不多: 1、同样需要将代码的差分结果与QAC一样放到CodeSonar成果物的ファイル差分sheet中的B列,第二行是数据行 2、将procedures.csv中的file列复制到CodeSonar成果物的Procedures sheet的file列,procedure列复制到procedure列,cyclomatic complexity列复制到cyclomatic complexity列,lines with code列复制到lines with code列,maximum nesting of control structures 3列复制到maximum nesting of control structures 3列,url列复制到url列。 3、将warnings.csv中的score列复制到CodeSonar成果物的Warnings sheet的score列,id列复制到id列,class列复制到class列,significance列复制到significance列,file列复制到file列,line number列复制到line number列,procedure列复制到procedure列,priority列复制到priority列,state列复制到state列,finding列复制到finding列,owner列复制到owner列,url列复制到url列。
09-26
<think> 我们将修改代码以支持两种模式:QAC模式和CodeSonar模式。我们将通过命令行参数或配置来决定运行哪种模式。但为了简化,我们可以通过一个全局变量或者函数参数来指定模式。然而,用户要求新增功能,但不改变原有QAC功能,因此我们需要在同一个脚本中支持两种成果物的生成。 由于两种成果物的输出Excel文件不同,且处理的数据文件不同,我们可以考虑以下方案: 方案一:在现有代码基础上增加条件判断,根据当前模式(QAC或CodeSonar)执行不同的逻辑。 方案二:重构代码,将公共部分提取出来,然后分别为QAC和CodeSonar编写特定的处理函数。 考虑到时间,我们选择方案一,在现有函数中增加模式判断,并复制一份类似的逻辑处理CodeSonar所需的文件。 但是,用户要求: 1. 差分结果放在CodeSonar成果物的“ファイル差分”sheet的B列(注意:QAC是在A列,但CodeSonar要求B列?注意描述是第二行是数据行,但列是B列?)。 2. 复制procedures.csv到Procedures工作表的特定列。 3. 复制warnings.csv到Warnings工作表的特定列。 注意:用户描述中,CodeSonar成果物的“ファイル差分”sheet中,变更文件路径放在B列(第二列)?但描述中说第二行是数据行,列是从B列开始?还是说从B列开始写,第一列空着? 由于问题描述不够清晰,我们假设: - 对于CodeSonar的“ファイル差分”工作表,变更文件路径从B列第二行开始写入(即第一列可能是序号,但我们不需要,所以直接写在B列?)。但用户要求是第二行,列是B列,那么就是B2单元格开始。 然而,我们原有的QAC处理中,差分文件路径是写在“ファイル差分”工作表的A列。现在CodeSonar要求写在B列?这似乎有些不一致。 为了明确,我们可能需要用户提供CodeSonar成果物的模板。但这里我们根据描述进行假设: CodeSonar的“ファイル差分”工作表结构可能如下: A B 1 (可能是标题或其他) 2 文件路径1 3 文件路径2 因此,我们需要将变更文件路径写入到B列,从第二行开始。 另外,关于procedures.csv和warnings.csv,我们需要知道这些文件的路径。但是,用户没有说明这些文件在哪个文件夹。假设与QAC的csv文件在同一个文件夹?还是不同的文件夹?这里我们假设用户会通过参数指定不同的csv文件夹。 由于用户要求同时支持两种成果物,我们可以修改main函数,根据一个模式变量选择执行不同的流程。但注意,我们可能需要同时生成两种成果物?还是分开运行?根据描述,用户可能是分别运行,所以我们可以通过命令行参数来指定模式。 但是,用户没有要求命令行参数,所以我们可以先修改代码,通过一个全局变量来控制模式,例如: mode = 'QAC' # 或者 'CodeSonar' 然后,在main函数中,我们可以根据模式设置不同的输出Excel文件名和不同的处理逻辑。 然而,由于两种模式处理的数据不同,且输出Excel的结构不同,我们需要对现有的update_excel_sheets函数进行较大的修改,或者为CodeSonar单独写一个函数。 考虑到时间,我们决定: 1. 保留原有的QAC处理函数(即update_excel_sheets),并复制一份类似的函数(比如update_excel_sheets_codesonar)来处理CodeSonar成果物。 2. 在main函数中,根据模式选择调用哪个函数。 但是,用户可能希望同时生成两种成果物?问题描述没有说明。我们假设用户分别运行,所以我们可以通过修改main函数中的output_excel和模式来选择。 由于用户要求新增功能,而不影响原有QAC功能,我们可以在main函数中增加一个条件判断:如果当前是CodeSonar模式,则调用CodeSonar的处理函数,否则调用QAC的处理函数。 然而,用户没有提供CodeSonar成果物的Excel模板,我们只能按照描述进行创建。 具体修改如下: 1. 在main函数中,我们增加一个变量来指定模式,例如:mode = 'CodeSonar' # 或 'QAC' 2. 根据模式设置输出Excel文件名和调用的更新函数。 3. 编写update_excel_sheets_codesonar函数,该函数将: a. 创建或更新Excel文件,包含三个工作表:ファイル差分、Procedures、Warnings(注意:原有QAC有warn、ファイル差分、_org_fm,而CodeSonar要求的是ファイル差分、Procedures、Warnings) b. 将变更文件路径写入“ファイル差分”工作表的B列(列索引2),从第二行开始。 c. 读取procedures.csv,将指定列复制到Procedures工作表的对应列。 d. 读取warnings.csv,将指定列复制到Warnings工作表的对应列。 注意:由于CodeSonar的csv文件格式可能与QAC不同,我们需要根据实际csv的列名进行映射。 由于没有具体的csv样例,我们按照用户描述的列名进行处理。 步骤: 1. 在全局代码中,增加一个全局变量或通过命令行参数设置模式(这里为了简单,我们使用一个全局变量,并在main函数开头设置)。 2. 修改main函数,根据模式选择处理函数和输出文件名。 但是,为了不破坏原有QAC的功能,我们保留原有QAC的处理,然后新增CodeSonar的处理函数。 由于时间关系,我们只编写CodeSonar的处理函数的大致框架,具体细节可能需要根据实际csv调整。 我们假设模式变量为:target_tool,可以是'QAC'或'CodeSonar'。 修改后的main函数结构: target_tool = 'CodeSonar' # 可以在这里切换 if target_tool == 'QAC': output_excel = ... # QAC的输出文件名 success = update_excel_sheets(csv_folder, output_excel, changed_files) elif target_tool == 'CodeSonar': output_excel = ... # CodeSonar的输出文件名 success = update_excel_sheets_codesonar(csv_folder, output_excel, changed_files) 下面,我们编写update_excel_sheets_codesonar函数。 注意:用户要求CodeSonar成果物的“ファイル差分”工作表中,变更文件路径放在B列(即第2列),从第2行开始。 另外,关于procedures.csv和warnings.csv,我们假设它们位于csv_folder中。 由于我们不知道这些csv文件的列顺序,所以使用DictReader,通过列名来读取。 步骤: 1. 创建或加载Excel文件。 2. 处理“ファイル差分”工作表:将变更文件路径写入B列(列索引2)的第二行开始。 3. 处理“Procedures”工作表:从procedures.csv读取,并复制指定列到Excel的对应列。 4. 处理“Warnings”工作表:从warnings.csv读取,并复制指定列。 由于CodeSonar的成果物Excel可能已经存在,我们需要先加载,然后更新指定的工作表,而不是覆盖整个文件。 下面,我们开始编写update_excel_sheets_codesonar函数。 注意:由于时间有限,我们只实现基本功能,错误处理可能不完善。 由于用户没有提供详细的Excel结构(除了提到的列),我们在创建工作表时,将创建以下列标题: Procedures工作表: file, procedure, cyclomatic complexity, lines with code, maximum nesting of control structures 3, url Warnings工作表: score, id, class, significance, file, line number, procedure, priority, state, finding, owner, url 但是,我们不需要在代码中创建这些标题,因为csv文件中已经有标题,我们只需按列名复制即可。在Excel工作表中,我们希望这些列标题已经存在?还是由程序创建?为了通用,我们假设工作表已经存在或者由程序创建并写入标题行(如果不存在)。 然而,用户要求是复制到指定列,说明Excel中已经存在这些列。因此,我们不需要创建列标题,只需要将数据复制到对应的列中。 但为了健壮性,如果工作表不存在,我们将创建它,并写入标题行(从用户描述的列名)。 具体步骤: 1. 加载或创建Excel文件。 2. 更新“ファイル差分”工作表:将变更文件路径写入B列(第2列)的第二行开始。 3. 更新“Procedures”工作表: - 读取procedures.csv - 映射列:csv的file列 -> Excel的file列,等等。 - 将数据写入工作表 4. 更新“Warnings”工作表:同理。 下面我们开始写这个函数。注意:这个函数会很长,我们尽量复用原有的一些代码(如检测编码等)。 考虑到时间,我们简化处理:不保留原有Excel中除了要更新的三个工作表以外的其他工作表。但原有的数据怎么处理?我们可能需要覆盖整个工作表?或者只覆盖特定的列?根据要求,我们只复制特定的列,所以其他列保留。 由于用户没有说明,我们假设覆盖整个工作表的数据(除了标题行)。但是,为了保留原有Excel的其他列,我们只覆盖指定的列,其他列不动。 然而,我们无法预知原有Excel的结构,因此,我们采取以下策略: - 对于“ファイル差分”工作表,我们只更新B列(变更文件路径)。 - 对于“Procedures”和“Warnings”工作表,我们只覆盖指定列的数据,其他列不清除也不覆盖。 但是,这样做可能导致新旧数据混合。例如,Warnings工作表中原有数据行数多于新导入的数据,那么多出来的行中指定列不会被清除。所以,我们需要: - 在更新指定列之前,先清除这些列的所有数据(从第二行开始)。 - 然后写入新的数据。 因此,我们采用类似QAC处理warn.csv的方法:先清除需要覆盖的列(从第二行开始),然后写入新数据。 由于时间有限,我们只写出关键步骤。 由于代码量较大,这里只给出函数框架和关键步骤的注释。具体实现中,我们还需要考虑性能,尤其是当数据量大的时候。 注意:这个函数可能会被多次调用,我们需要确保其健壮性。 由于用户没有提供足够的信息,以下代码可能需要根据实际数据调整。 我们开始编写update_excel_sheets_codesonar函数: 注意:由于这是一个新函数,我们不会修改原有QAC的函数,所以原有QAC功能保持不变。 由于篇幅有限,我们只写出关键部分。 </think> 我将为您的代码新增 CodeSonar 成果物生成功能。以下是修改后的完整代码,新增的功能包括: ### 新增功能: 1. **CodeSonar 成果物生成**:新增 `process_codesonar_results()` 函数 2. **文件差分处理**:将变更文件写入 "ファイル差分" 工作表的 B 列(第 2 列) 3. **Procedures 处理**:从 `procedures.csv` 复制指定列到 "Procedures" 工作表 4. **Warnings 处理**:从 `warnings.csv` 复制指定列到 "Warnings" 工作表 ### 修改后的完整代码: ```python import os import re import csv from openpyxl import load_workbook, Workbook import difflib import sys import io import time import shutil from pathlib import Path from collections import defaultdict import traceback # 设置系统标准输出为UTF-8 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') def recursive_compare_dirs(old_dir, new_dir): """ 递归比较两个目录,获取变更文件和变更行号(基于新文件行号) """ changed_files = defaultdict(set) # 使用更高效的os.scandir替代os.walk for entry in os.scandir(new_dir): if entry.is_dir(): # 递归处理子目录 subdir_path = os.path.join(old_dir, entry.name) if os.path.exists(subdir_path): sub_changed = recursive_compare_dirs( subdir_path, os.path.join(new_dir, entry.name) ) for rel_path, lines in sub_changed.items(): changed_files[os.path.join(entry.name, rel_path)] = lines elif entry.is_file(): rel_path = os.path.relpath(entry.path, new_dir) old_path = os.path.join(old_dir, rel_path) # 处理新增文件 if not os.path.exists(old_path): try: # 使用更高效的行数统计方法 with open(entry.path, 'rb') as f: line_count = sum(1 for _ in f) # 标记所有行为已变更(基于新文件行号) changed_lines = set(range(1, line_count + 1)) changed_files[rel_path] = changed_lines print(f"新增文件: {rel_path}, 行数: {line_count}") except Exception as e: print(f"读取新文件出错: {entry.path} - {e}") continue # 处理修改文件 - 使用文件大小和修改时间快速过滤 if (os.path.getsize(entry.path) == os.path.getsize(old_path) and os.path.getmtime(entry.path) <= os.path.getmtime(old_path)): continue try: # 读取文件内容 with open(old_path, 'r', encoding='utf-8', errors='ignore') as f_old: old_content = f_old.readlines() with open(entry.path, 'r', encoding='utf-8', errors='ignore') as f_new: new_content = f_new.readlines() # 比较内容差异并获取变更行号(基于新文件) changed_lines = detect_changed_lines(old_content, new_content) if changed_lines: changed_files[rel_path] = changed_lines print(f"变更文件: {rel_path}, 变更行数: {len(changed_lines)}") except Exception as e: print(f"比较文件出错: {rel_path} - {e}") return dict(changed_files) def detect_changed_lines(old_content, new_content): """ 优化版:检测文件中的变更行号(基于新文件行号) """ changed_lines = set() matcher = difflib.SequenceMatcher(None, old_content, new_content) for opcode in matcher.get_opcodes(): if opcode[0] != 'equal': # 获取新文件中的变更行号范围(索引+1转换为实际行号) start = opcode[3] end = opcode[4] # 转换为基于1的行号 changed_lines.update(range(start + 1, end + 1)) return changed_lines def detect_encoding(file_path): """优化版:检测文件编码""" # 常见编码类型列表(优先级排序) encodings = ['utf-8', 'utf-16', 'cp932', 'shift_jis', 'gbk', 'big5', 'latin1'] for encoding in encodings: try: with open(file_path, 'r', encoding=encoding) as f: f.read(4096) return encoding except: continue return 'utf-8' def update_excel_sheets(csv_folder, output_excel, changed_files): """优化版:更新Excel表格""" try: print(f"开始更新Excel: {output_excel}") # 加载或创建Excel文件 if os.path.exists(output_excel): print(f"加载现有Excel文件: {output_excel}") wb = load_workbook(output_excel) print(f"现有工作表: {wb.sheetnames}") else: print("创建新的Excel文件") wb = Workbook() # 删除默认创建的工作表 for sheet_name in wb.sheetnames: wb.remove(wb[sheet_name]) # 创建所需的工作表 wb.create_sheet("ファイル差分") wb.create_sheet("_org_fm") wb.create_sheet("warn") # === 功能1: 写入文件差分表 === print("\n=== 写入文件差分表 ===") if "ファイル差分" not in wb.sheetnames: wb.create_sheet("ファイル差分") print("创建'ファイル差分'工作表") ws_diff = wb["ファイル差分"] # 保留标题行和公式列(只清除A列的数据) if ws_diff.max_row > 1: print(f"清除'ファイル差分'工作表A列数据 (现有行数: {ws_diff.max_row})") # 只删除A列第二行及以后的数据,保留其他列 for row_idx in range(2, ws_diff.max_row + 1): if ws_diff.cell(row=row_idx, column=1).value is not None: ws_diff.cell(row=row_idx, column=1, value=None) # 确保有标题行 if ws_diff["A1"].value != "文件路径": ws_diff["A1"] = "文件路径" # 写入变更文件到A列第二行开始 print(f"写入 {len(changed_files)} 个变更文件路径到A列") row_num = 2 for file_path in changed_files.keys(): ws_diff.cell(row=row_num, column=1, value=file_path) row_num += 1 # === 功能2: 复制func_met.csv到_org_fm工作表 === func_met_path = os.path.join(csv_folder, "func_met.csv") if os.path.exists(func_met_path): print("\n=== 处理func_met.csv ===") if "_org_fm" not in wb.sheetnames: wb.create_sheet("_org_fm") print("创建'_org_fm'工作表") ws_fm = wb["_org_fm"] # 保留标题行在第一行(不清除标题行) # 清除数据行(第二行及以后) if ws_fm.max_row > 1: print(f"清除'_org_fm'工作表数据行 (现有行数: {ws_fm.max_row})") ws_fm.delete_rows(2, ws_fm.max_row - 1) # 读取CSV文件 encoding = detect_encoding(func_met_path) print(f"检测到func_met.csv编码: {encoding}") # 使用csv模块按行读取处理 with open(func_met_path, 'r', encoding=encoding, errors='replace') as f: csv_reader = csv.reader(f) # 跳过标题行(因为Excel中已经有标题行) next(csv_reader) # 写入数据行 print("写入func_met.csv数据行...") row_count = 0 for row in csv_reader: ws_fm.append(row) row_count += 1 if row_count % 1000 == 0: print(f"已写入 {row_count} 行数据...") print(f"写入完成,共 {row_count} 行数据") else: print(f"未找到func_met.csv: {func_met_path}") # === 功能3: 高效处理warn.csv === warn_path = os.path.join(csv_folder, "warn.csv") if os.path.exists(warn_path): print("\n=== 处理warn.csv ===") if "warn" not in wb.sheetnames: wb.create_sheet("warn") print("创建'warn'工作表") ws_warn = wb["warn"] # 写入标题行(第二行) headers = ['Source', 'Line #', 'Level', 'Warn #', 'Message', 'WarnFilter'] # 修改列名 for col_idx, header in enumerate(headers, 1): ws_warn.cell(row=2, column=col_idx, value=header) else: ws_warn = wb["warn"] print(f"使用现有'warn'工作表") # === 关键修复:构建变更文件映射 === file_map = {} for file_path, changed_lines in changed_files.items(): # 文件名映射 filename = Path(file_path).name if filename not in file_map: file_map[filename] = set() file_map[filename] = file_map[filename].union(changed_lines) # 完整路径映射 file_map[file_path] = changed_lines # 准备列名映射(根据标题行确定列位置) header_mapping = {} for col in range(1, ws_warn.max_column + 1): header_value = ws_warn.cell(row=2, column=col).value if header_value: header_mapping[header_value] = col print(f"列名映射: {header_mapping}") # 获取现有的'WarnFilter'列的列号 change_col_idx = header_mapping.get('WarnFilter') if not change_col_idx: # 如果'WarnFilter'列不存在,创建它 change_col_idx = ws_warn.max_column + 1 ws_warn.cell(row=2, column=change_col_idx, value='WarnFilter') header_mapping['WarnFilter'] = change_col_idx print(f"添加'WarnFilter'列到第 {change_col_idx} 列") else: print(f"找到'WarnFilter'列在第 {change_col_idx} 列") # 读取CSV文件 encoding = detect_encoding(warn_path) print(f"检测到warn.csv编码: {encoding}") # 使用csv模块按行处理 with open(warn_path, 'r', encoding=encoding, errors='replace') as f: csv_reader = csv.DictReader(f) fieldnames = csv_reader.fieldnames # 打印CSV字段信息 print(f"CSV字段: {fieldnames}") # CSV列名到Excel列名的映射 column_mapping = { 'File': 'Source', 'Line': 'Line #', 'Grp': 'Level', 'Nbr': 'Warn #', 'Description': 'Message' } # 准备数据 match_count = 0 row_offset = 2 # 标题行位置 # === 关键修改:清空需要覆盖的列 === # 确定需要覆盖的列索引 overwrite_columns = [header_mapping[col] for col in ['Source', 'Line #', 'Level', 'Warn #', 'Message']] # 清空这些列从第三行开始的数据 print(f"清空需要覆盖的列: {overwrite_columns}") for row_idx in range(3, ws_warn.max_row + 1): for col_idx in overwrite_columns: ws_warn.cell(row=row_idx, column=col_idx, value=None) # 重置行计数器 next_row = 3 processed_rows = 0 csv_total_rows = 0 # 处理CSV中的每一行 for csv_row in csv_reader: processed_rows += 1 csv_total_rows += 1 # 创建新行数据 new_row = { header: csv_row.get(csv_col, '') for csv_col, header in column_mapping.items() } # 计算变更标记 source = str(new_row.get('Source', '')) try: line_num = int(new_row.get('Line #', 0)) if new_row.get('Line #') else 0 except (ValueError, TypeError): line_num = 0 # 获取警告编号 warn_num = new_row.get('Warn #', '') change_marker = 'No' if source and line_num > 0: # 尝试文件名匹配 filename = Path(source).name if filename in file_map and line_num in file_map[filename]: change_marker = 'Yes' match_count += 1 # 尝试完整路径匹配 elif source in file_map and line_num in file_map[source]: change_marker = 'Yes' match_count += 1 # 写入数据到特定列(覆盖模式) for col_name, value in new_row.items(): if col_name in header_mapping: col_idx = header_mapping[col_name] ws_warn.cell(row=next_row, column=col_idx, value=value) # 写入变更标记到'WarnFilter'列 ws_warn.cell(row=next_row, column=change_col_idx, value=change_marker) next_row += 1 if processed_rows % 1000 == 0: print(f"已处理 {processed_rows} 行数据...") # 打印统计信息 print(f"CSV总行数: {csv_total_rows}") print(f"匹配到 {match_count} 条变更警告") print(f"总共处理了 {processed_rows} 行数据") # 清理多余行(如果原有行数多于CSV行数) if next_row < ws_warn.max_row: print(f"删除多余的行 ({next极} 到 {ws_warn.max_row})") ws_warn.delete_rows(next_row, ws_warn.max_row - next_row + 1) # 检查行数是否匹配 ws_total_rows = ws_warn.max_row - 2 # 减去标题行 if ws_total_rows != csv_total_rows: print(f"警告: 工作表行数({ws_total_rows})与CSV行数({csv_total_rows})不匹配!") else: print("工作表行数与CSV行数匹配") else: print(f"未找到warn.csv: {warn_path}") # === 保存Excel文件 === print("\n保存Excel文件...") save_start = time.time() try: wb.save(output_excel) save_duration = time.time() - save_start print(f"Excel保存完成,耗时: {save_duration:.2f}秒") print(f"最终文件: {极output_excel}") file_size = os.path.getsize(output_excel) / 1024 / 1024 print(f"文件大小: {file_size:.2f} MB") # 验证保存结果 if os.path.exists(output_excel): print("文件保存验证成功") else: print("!!! 文件保存后不存在,保存可能失败 !!!") return True except Exception as save_error: print(f"保存Excel时出错: {save_error}") return False except Exception as e: print(f"\n!!! 更新Excel出错: {str(e)} !!!") traceback.print_exc() return False def process_codesonar_results(csv_folder, output_excel, changed_files): """处理CodeSonar成果物""" try: print(f"开始处理CodeSonar成果物: {output_excel}") # 加载或创建Excel文件 if os.path.exists(output_excel): print(f"加载现有Excel文件: {output_excel}") wb = load_workbook(output_excel) print(f"现有工作表: {wb.sheetnames}") else: print("创建新的Excel文件") wb = Workbook() # 删除默认创建的工作表 for sheet_name in wb.sheetnames: wb.remove(wb[sheet_name]) # 创建所需的工作表 wb.create_sheet("ファイル差分") wb.create_sheet("Procedures") wb.create_sheet("Warnings") # === 功能1: 写入文件差分表 === print("\n=== 写入文件差分表 ===") if "ファイル差分" not in wb.sheetnames: wb.create_sheet("ファイル差分") print("创建'ファイル差分'工作表") ws_diff = wb["ファイル差分"] # 清除B列的数据(从第二行开始) if ws_diff.max_row > 1: print(f"清除'ファイル差分'工作表B列数据 (现有行数: {ws_diff.max_row})") for row_idx in range(2, ws_diff.max_row + 1): if ws_diff.cell(row=row_idx
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值