为了账号安全,请及时绑定邮箱和手机立即绑定

如何修复使用#2nd 记录而不是#1st(#3rd 而不是#2nd 等)的游标?

如何修复使用#2nd 记录而不是#1st(#3rd 而不是#2nd 等)的游标?

HUH函数 2022-11-02 16:49:24
布局只是 main.xml,它在将新信息添加到 sqlite 数据库时显示多个 row.xml。单击按钮时,将显示带有数据的 AlertDialog。问题是,当我单击行中的按钮时,AlertDialog 正在从数据库中的下一条记录(下一行)获取数据(当我单击第一个按钮时,AD 正在获取第二条记录等),而不是从按钮所在的位置获取数据。我为按钮和 toast 消息设置标签,单击它时显示良好的 ID。MyCursorAdapter.java(...)@Overridepublic void bindView(View view, Context context, Cursor cursor) {TextView name = (TextView) view.findViewById(R.id.name);TextView id = (TextView) view.findViewById(R.id.id);TextView quantity = (TextView) view.findViewById(R.id.quantity);TextView mu = (TextView) view.findViewById(R.id.mu);Button bt = (Button) view.findViewById(R.id.rowalertdialog);CheckBox cb = (CheckBox)view.findViewById(R.id.checkboxmain2);name.setText(cursor.getString(cursor.getColumnIndex(SQLiteAdapter.KEY_NAME)));id.setText(cursor.getString(cursor.getColumnIndex(SQLiteAdapter._id)));quantity.setText(cursor.getString(cursor.getColumnIndex(SQLiteAdapter.KEY_QUANTITY)));mu.setText(cursor.getString(cursor.getColumnIndex(SQLiteAdapter.KEY_MU)));bt.setTag(cursor.getString(cursor.getColumnIndex(SQLiteAdapter._id)));cb.setChecked(cursor.getInt(cursor.getColumnIndex(SQLiteAdapter.KEY_CHECKED)) > 0);cb.setTag(cursor.getString(cursor.getColumnIndex(SQLiteAdapter._id)));}安卓SQLite.java(...)private void manageListView() {cursor = mySQLiteAdapter.queueAll();if(myadapter == null){    myadapter = new MyCursorAdapter(this,cursor);    listContent.setAdapter(myadapter);}else {    myadapter.swapCursor(cursor);}}
查看完整描述

2 回答

?
PIPIONE

TA贡献1829条经验 获得超9个赞

工作示例

这是一个基于您之前的问题和这个问题的工作示例应用程序。

然而,

  • 按钮单击处理,在不使用布局的onCLick(如 XML 中编码)的情况下进行处理,但在自定义CursorAdapter ( MyCursorAdapter.java ) 的bindView方法中设置。

  • 它演示了 3 种获取与按钮关联的 id 的方法

    • item_id_fromcursor显示了如何从作为列表源的 Cursor 获取值(在本例中为该 id),该列表的位置应适当。

    • item_id_fromtag展示了如何从标签中获取 id(有些人不赞成这种技术)。

    • item_id_fromview展示了如何根据传递给按钮的视图从视图层次结构中获取值。

该处理允许删除和更新行(尽管更新非常基本,只是附加一个值而不是使用自定义对话框布局(您似乎掌握了这方面的知识))。

与前面的答案不同,还实现了处理添加的行(尽管值有限)。

生成的应用程序看起来像:-

//img1.sycdn.imooc.com//63622f3300010cd905840854.jpg

  • A表示行已被删除(即 id 为 5-7 的行不存在)

  • B显示已更新的行(即已附加更新(如果更新更新则该行已更新两次))

  • C显示已添加的行。

如果单击编辑或删除按钮,则会出现对话框,例如:-

//img1.sycdn.imooc.com//63622f3e000129fc04850225.jpg

  • 单击取消从对话框中返回,什么也不做。

  • 单击删除将删除相应的行(如对话框消息中所述)。

  • 单击 EDIT 通过附加更新的值来编辑名称(编辑已经编辑的行将添加进一步更新的行)

    • 显然这可能不是所需的操作,它只是为了演示。

编码

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    >

    <TextView

        android:id="@+id/panelup"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="LIST SQ1"

        />

    <ListView

        android:id="@+id/contentlist"

        android:layout_width="fill_parent"

        android:layout_height="fill_parent"

        android:layout_below="@id/panelup"

        android:layout_above="@id/paneldown"/>

    <LinearLayout

        android:id="@+id/paneldown"

        android:orientation="horizontal"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:layout_alignParentBottom="true">

        <EditText

            android:id="@+id/name"

            android:layout_width="match_parent"

            android:layout_height="wrap_content"

            android:layout_weight="1"

            />

        <EditText

            android:id="@+id/quantity"

            android:layout_width="match_parent"

            android:layout_height="wrap_content"

            android:inputType="number"

            android:layout_weight="2"

            />

        <Spinner

            android:id="@+id/mu"

            android:layout_width="match_parent"

            android:layout_height="wrap_content"

            android:entries="@array/mu_values"

            android:layout_weight="2"

            />

        <Button

            android:id="@+id/add"

            android:layout_width="match_parent"

            android:layout_height="wrap_content"

            android:layout_weight="2"

            android:text="+"

            />

    </LinearLayout>

</LinearLayout>

行.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="fill_parent"

    android:layout_height="wrap_content">

    <LinearLayout

        android:orientation="horizontal"

        android:layout_width="fill_parent"

        android:layout_height="wrap_content"

        android:id="@+id/layoutmain"

        >

        <TextView

            android:layout_width="wrap_content"

            android:layout_height="fill_parent"

            android:padding="2dip"

            android:text="M"/>

        <TextView

            android:id="@+id/id"

            android:layout_width="wrap_content"

            android:layout_height="fill_parent"

            android:padding="2dip"

            android:paddingRight="10dip"/>

        <TextView

            android:layout_width="wrap_content"

            android:layout_height="fill_parent"

            android:padding="2dip"

            android:paddingRight="10dip"

            android:text="-" />

        <TextView

            android:id="@+id/name"

            android:layout_width="fill_parent"

            android:layout_height="fill_parent"

            android:padding="2dip"/>

    </LinearLayout>

    <TextView

        android:id="@+id/quantity"

        android:layout_width="fill_parent"

        android:layout_height="wrap_content"

        android:padding="2dip"/>

    <CheckBox

        android:id="@+id/checkboxmain2"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:visibility="gone"/>

    <Button

        android:id="@+id/editordelete"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="EDIT or DELETE"/>

</LinearLayout>

注意 CheckBox 已隐藏

SQLiteHelper.java

public class SQLiteHelper extends SQLiteOpenHelper {


    public static final String MYDATABASE_NAME = "mydatabase";

    public static final int  MYDATABASE_VERSION = 1;

    public static final String MYDATABASE_TABLE = "mytable";


    SQLiteDatabase mDB;


    public SQLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,int version) {

        super(context, name, factory, version);

        mDB = this.getWritableDatabase();

    }


    @Override

    public void onCreate(SQLiteDatabase db) {

        String crt_tbl_sql = "CREATE TABLE IF NOT EXISTS " + MYDATABASE_TABLE + "(" +

                SQLiteAdapter._id + " INTEGER PRIMARY KEY, " +

                SQLiteAdapter.KEY_NAME + " TEXT, " +

                SQLiteAdapter.KEY_SHOP + " TEXT, " +

                SQLiteAdapter.KEY_PDATE + " TEXT, " +

                SQLiteAdapter.KEY_PRICE + " REAL, " +

                SQLiteAdapter.KEY_QUANTITY + " INTEGER, " +

                SQLiteAdapter.KEY_MU + " TEXT, " +

                SQLiteAdapter.KEY_CHECKED + " INTEGER DEFAULT 0" +

                ")";

        db.execSQL(crt_tbl_sql);

        addSomeTestingData(db,10);

    }


    @Override

    public void onConfigure(SQLiteDatabase db) {

        super.onConfigure(db);

        db.setForeignKeyConstraintsEnabled(true);

    }


    @Override

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {


    }


    public long addRow(String name, String shop, String pdate, double price, int quantity, String mu, SQLiteDatabase db) {

        ContentValues cv = new ContentValues();

        cv.put(SQLiteAdapter.KEY_NAME,name);

        cv.put(SQLiteAdapter.KEY_SHOP,shop);

        cv.put(SQLiteAdapter.KEY_PDATE,pdate);

        cv.put(SQLiteAdapter.KEY_PRICE,price);

        cv.put(SQLiteAdapter.KEY_QUANTITY,quantity);

        cv.put(SQLiteAdapter.KEY_MU,mu);

        return db.insert(MYDATABASE_TABLE,null,cv);

    }


    private void addSomeTestingData(SQLiteDatabase db, int number_to_add) {


        for (int i = 0; i < number_to_add;i++) {

            String suffix = String.valueOf(i);

            String day_in_month = suffix;

            if (i < 10) {

                day_in_month = "0" + day_in_month;

            }

            addRow(

                    "Test" + suffix,

                    "Shop" + suffix,

                    "2019-01-" + day_in_month,

                    10.5 + new Double(i * 3),

                    i * 4,

                    "mu" + suffix,

                    db

            );

        }

    }

}

SQLiteAdapter.java

public class SQLiteAdapter {


    SQLiteDatabase sqLiteDatabase;

    SQLiteHelper sqLiteHelper;

    Context context;


    public static final String KEY_CHECKED = "checked";

    public static final String _id = BaseColumns._ID;

    public static final String KEY_NAME = "name";

    public static final String KEY_QUANTITY = "quantity";

    public static final String KEY_PRICE = "price";

    public static final String KEY_MU = "mu";

    public static final String KEY_PDATE = "pdate";

    public static final String KEY_SHOP = "shop";


    public SQLiteAdapter(Context context) {

        this.context = context;

        openToWrite();

    }


    public SQLiteAdapter openToWrite() throws android.database.SQLException {

        sqLiteHelper = new SQLiteHelper(context, MYDATABASE_NAME, null,

                MYDATABASE_VERSION);

        sqLiteDatabase = sqLiteHelper.getWritableDatabase();

        return this;

    }



    public void close() {

        sqLiteHelper.close();

    }


    public long insertChecked(boolean data1) {


        ContentValues contentValues = new ContentValues();

        contentValues.put(KEY_CHECKED, data1);

        return sqLiteDatabase.insert(MYDATABASE_TABLE, null, contentValues);

    }


    public int updateChecked(long id,int check) {

        ContentValues cv = new ContentValues();

        cv.put(KEY_CHECKED,check);

        String whereclause = _id + "=?";

        String[] whereargs = new String[]{String.valueOf(id)};

        return sqLiteDatabase.update(MYDATABASE_TABLE,cv,whereclause,whereargs);

    }


    public Cursor queueAll() {

        String[] columns = new String[]{_id, KEY_NAME, KEY_PRICE,

                KEY_QUANTITY, KEY_MU,

                KEY_PDATE, KEY_SHOP, KEY_CHECKED};

        Cursor cursor = sqLiteDatabase.query(MYDATABASE_TABLE, columns,

                null, null, null, null, null);

        return cursor;

    }


    public Cursor queueOneById(long id) {

        String whereclause = _id + "=?";

        String[] whereargs = new String[]{String.valueOf(id)};

        String[] columns = new String[]{_id, KEY_NAME, KEY_PRICE,

                KEY_QUANTITY, KEY_MU,

                KEY_PDATE, KEY_SHOP, KEY_CHECKED};

        return sqLiteDatabase.query(MYDATABASE_TABLE,columns,whereclause,whereargs,

                null,null,null);


    }


    public int delete(long id) {

        String whereclause = SQLiteAdapter._id + "=?";

        String[] wherargs = new String[]{String.valueOf(id)};

        return sqLiteDatabase.delete(MYDATABASE_TABLE,whereclause,wherargs);

    }


    public long updateById(long id, String column_to_change, String newvalue) {

        ContentValues cv = new ContentValues();

        cv.put(column_to_change,newvalue);

        String whereclause = SQLiteAdapter._id + "=?";

        String[] whereargs = new String[]{String.valueOf(id)};

        return sqLiteDatabase.update(MYDATABASE_TABLE,cv,whereclause,whereargs);

    }

}

增加了一些方法

MyCursorAdapter.java

public class MyCursorAdapter extends CursorAdapter {


    SQLiteAdapter sqliteAdapter;

    MyCursorAdapter thisCursorAdapter;



    public MyCursorAdapter(Context context, Cursor c) {


        super(context, c, true);

        sqliteAdapter = new SQLiteAdapter(context);

        thisCursorAdapter = this;

    }


    @Override

    public View getView(int position, View convertView, ViewGroup parent) {

        View v = super.getView(position, convertView, parent);

        return v;

    }


    @Override

    public View newView(Context context, Cursor cursor, ViewGroup parent) {

        return LayoutInflater.from(context).inflate(R.layout.row,parent,false);

    }


    @Override

    public void bindView(View view, Context context, final Cursor cursor) {


        //Note Cursor will be positioned appropriately

        TextView name = (TextView) view.findViewById(R.id.name);

        TextView id = (TextView) view.findViewById(R.id.id);

        TextView quantity = (TextView) view.findViewById(R.id.quantity);

        CheckBox cb = (CheckBox) view.findViewById(R.id.checkboxmain2);

        Button eod = (Button) view.findViewById(R.id.editordelete);


        name.setText(cursor.getString(cursor.getColumnIndex(SQLiteAdapter.KEY_NAME)));

        id.setText(cursor.getString(cursor.getColumnIndex(SQLiteAdapter._id)));

        quantity.setText(cursor.getString(cursor.getColumnIndex(SQLiteAdapter.KEY_QUANTITY)));

        cb.setChecked(cursor.getInt(cursor.getColumnIndex(SQLiteAdapter.KEY_CHECKED)) > 0);

        cb.setTag(cursor.getString(cursor.getColumnIndex(SQLiteAdapter._id))); //<<<<<<<<<< SET TAG to the ID

        eod.setTag(cursor.getString(cursor.getColumnIndex(SQLiteAdapter._id)));

        // dynamically add the listeners as opposed to coding onCLick in layout XML

        eod.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {


                // get the id from the id TextView, within the view hierarchy rather than from the buttons tag

                // NOTE assumes the Button's parent is a LinearLayout (for simplicity)


                // This in theory is the recommended way rather than setting the tag

                LinearLayout ll = (LinearLayout) v.getParent();

                TextView id = ll.findViewById(R.id.id), name = ll.findViewById(R.id.name);

                final long item_id_fromview = Long.valueOf(id.getText().toString());

                final String item_name = name.getText().toString();


                // get the id from the tag

                long item_id_fromtag = Long.valueOf(v.getTag().toString());

                // get the if from the cursor that is the source of the Listview, it should be positioned accordingly

                long item_id_fromcursor = cursor.getLong(cursor.getColumnIndex(SQLiteAdapter._id));


                // Show both

                Toast.makeText(v.getContext(),

                        "The id (from the view hierarchy) is " + String.valueOf(item_id_fromview) +

                                " or (from the tag) is " + String.valueOf(item_id_fromtag) +

                                " or (from the cursor) is" + String.valueOf(item_id_fromcursor)

                        , Toast.LENGTH_SHORT).show();

                AlertDialog.Builder mydialog = new AlertDialog.Builder(v.getContext());

                mydialog.setMessage("EDIT or DELETE Row:- ID: " + String.valueOf(item_id_fromview) + "Name: " + item_name);

                mydialog.setPositiveButton("DELETE", new DialogInterface.OnClickListener() {

                    @Override

                    public void onClick(DialogInterface dialog, int which) {

                        sqliteAdapter.delete(item_id_fromview);

                        refreshList();

                    }

                });

                mydialog.setNeutralButton("EDIT", new DialogInterface.OnClickListener() {

                    @Override

                    public void onClick(DialogInterface dialog, int which) {

                        sqliteAdapter.updateById(item_id_fromview,SQLiteAdapter.KEY_NAME,item_name + " Updated");

                        refreshList();

                    }

                });

                mydialog.setNegativeButton("CANCEL", new DialogInterface.OnClickListener() {

                    @Override

                    public void onClick(DialogInterface dialog, int which) {

                    }

                });

                mydialog.show();

            }

        });

    }

    private void refreshList() {

        thisCursorAdapter.swapCursor(sqliteAdapter.queueAll());

        thisCursorAdapter.notifyDataSetChanged();

    }

}

查看绑定视图时如何设置 onCLickListener

查看获取id的 3 种不同方法

请参阅类似于 AndroidSQLite 活动中使用的 ManageListView 方法的 refreshList 方法。

AndroidSQlite.java(活动)

public class AndroidSQLite extends AppCompatActivity {


    ListView listContent;

    Button buttonAdd;

    Cursor cursor;

    SQLiteAdapter mySQLiteAdapter;

    EditText name, quantity;

    Spinner mu;

    //SimpleCursorAdapter cursorAdapter; //<<<<<<<<<< NOT USED ANYMORE

    MyCursorAdapter myadapter; //<<<<<<<<<< Use a custom adapter that sets the tag of the checkbox to the respective id


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        listContent = (ListView) findViewById(R.id.contentlist);

        name = (EditText) findViewById(R.id.name);

        quantity = (EditText) findViewById(R.id.quantity);

        buttonAdd = (Button) findViewById(R.id.add);

        mu = (Spinner) findViewById(R.id.mu);

        handleAddButton();

        mySQLiteAdapter = new SQLiteAdapter(this);

        mySQLiteAdapter.openToWrite();

        manageListView(); //<<<<<<<<<< ADDED

    }


    //<<<<<<<<<< ADDED >>>>>>>>>>

    @Override

    protected void onResume() {

        super.onResume();

        manageListView(); //Refresh the List when resuming e.g. returning from another activity

    }


    @Override

    protected void onDestroy() {

        // TODO Auto-generated method stub

        super.onDestroy();

        cursor.close(); //<<<<<<<<<< SHOULD ALWAYS CLOSE CURSOR

        mySQLiteAdapter.close();

    }


    private void handleAddButton() {

        buttonAdd.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                if ((name.getText().toString()).length() < 1) {

                    Toast.makeText(v.getContext(),"Name cannot be empty",Toast.LENGTH_SHORT).show();

                    name.requestFocus();

                    return;

                }

                if ((quantity.getText().toString()).length() < 1) {

                    Toast.makeText(v.getContext(),"Quantity cannot be empty",Toast.LENGTH_SHORT).show();

                    quantity.requestFocus();

                    return;

                }

                mySQLiteAdapter.sqLiteHelper.addRow(

                        name.getText().toString(),

                        // Arbritary values for testing

                        "2019-01-01",

                        "The Shop",

                        100.33,

                        Integer.valueOf(quantity.getText().toString()),

                        mu.getSelectedItem().toString(),

                        mySQLiteAdapter.sqLiteDatabase

                );

                manageListView();

            }

        });


    }


    private void manageListView() {

        cursor = mySQLiteAdapter.queueAll(); // get the source data (cursor) for the listview

        if (myadapter == null) {

            myadapter = new MyCursorAdapter(this,cursor);

            listContent.setAdapter(myadapter);

        } else {

            myadapter.swapCursor(cursor);

        }

    }

}


查看完整回答
反对 回复 2022-11-02
?
慕工程0101907

TA贡献1887条经验 获得超5个赞

Cursor cursor = (Cursor) parent.getItemAtPosition(id); 将根据它在列表中的位置获取项目,但您使用的是 id(从按钮的标签中提取)作为它的位置。这将很少是正确的。


列表中的第一个位置是 0,第一个 id 将是 1(假设没有删除任何行并且您没有手动设置 id),因此您报告的症状。


如果行已被删除,那么每个缺失的 id 都会增加 id 和 position 之间的差异。如果行按 id 以外的任何其他顺序排序,则位置 v id 可能会出路。如果 where 子句不包括行,那么 id 和位置也会有所不同。


简单的事实是您不能依赖 id 和列表中的位置之间的任何关系。


您可以做的是使用标签中的 id 来查询数据库以提取特定行。该查询将与用于获取列表光标的查询相同,只是它将使用第三个 ( SQLiteAdapter._id + "=?") 和第四个参数 ( new String[]{String.valueOf(id)}) (如果使用查询便利方法)。


注释已更正(缺少S添加到 QLiteAdapter),因为评论。

已修改

我尝试添加我在上一条评论中写的代码,但这是错误的。我不知道如何实施您的更改。–


您的代码可能是:-


public void ListViewButtonHandler(View v){

    Button bt = v.findViewById(R.id.rowalertdialog);

    Toast.makeText(this, "You clicked the Button for ID " + (String)bt.getTag(), Toast.LENGTH_SHORT).show(); //TODO not needed


    int id = Integer.valueOf((String) bt.getTag());

    ListView parent = (ListView)findViewById(R.id.contentlist); //<<<<<<<<<< NOT NEEDED (I think)



    String whereclause = SQLiteAdapter._id _ "=?"; //<<<<<<<<<< ADDED

    String[] whereargs = new String[]{bt.getTag()}; //<<<<<<<<<< ADDED

    Cursor cursor = mySQLiteAdapter.getWitableDatabase().query(SQLiteAdapter.MYDATABASE_TABLE,null,whereclause,whereargs,null,null,null); //<<<<<<<<<< ADDED

    //Cursor cursor = (Cursor) parent.getItemAtPosition(id); //<<<<<<<<<< REMOVED/COMMENTED OUT

    //<<<<<<<<<<< ADDED to move to the extracted row, will toast if no such row

    if (!cursor.moveToFirst) {

        Toast.makeText(this, "Unable to retrieve row for ID " + (String)bt.getTag(), Toast.LENGTH_SHORT).show();

        return;

    }


    final int item_id = cursor.getInt(cursor.getColumnIndex(SQLiteAdapter._id));

    String item_name = cursor.getString(cursor.getColumnIndex(SQLiteAdapter.KEY_NAME));

    String item_quantity = cursor.getString(cursor.getColumnIndex(SQLiteAdapter.KEY_QUANTITY))

    cursor.close(); //<<<<<<<<<< ADDED should always close a cursor when done with it

    .......... the rest of the code

请注意评论


请注意,您可能需要将SQLiteAdapter.MYDATABASE_TABLE更改为适当的值。


注意上面的代码是in-principal code,它没有经过测试或运行,因此可能有一些错误


选择

如果您将以下方法(基于上一个问题)添加到 SQLiteAdapter.java :-


public Cursor queueOneById(long id) {

    String whereclause = _id + "=?";

    String[] whereargs = new String[]{String.valueOf(id)}; 

    String[] columns = new String[]{_id, KEY_NAME, KEY_PRICE,

            KEY_QUANTITY, KEY_MU,

            KEY_PDATE, KEY_SHOP, KEY_CHECKED};

    return sqLiteDatabase.query(MYDATABASE_TABLE,columns,whereclause,whereargs,

            null,null,null);


}

然后你可以改变


    Cursor cursor = mySQLiteAdapter.getWitableDatabase().query(SQLiteAdapter.MYDATABASE_TABLE,null,whereclause,whereargs,null,null,null);


    Cursor cursor = mySQLiteAdapter.queueOneById(id);

请注意,这仍然需要上述其他更改。那就是你需要:-


if (!cursor.moveToFirst) { Toast.makeText(this, "无法检索 ID 行" + (String)bt.getTag(), Toast.LENGTH_SHORT).show(); 返回; }


没有上面的代码。光标将位于位置-1(在第一行之前)。所以cursor.moveToFirst需要定位到提取的行(应该只有一行)。如果没有行,则 moveToFirst 将返回 false(无法完成移动)并且将发出 Toast,然后 onClick 方法将返回。


查看完整回答
反对 回复 2022-11-02
  • 2 回答
  • 0 关注
  • 83 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信