24 WHEN CAN WE STOP TESTING?

本文探讨了确定软件测试完成时机的复杂性,提出了五个常见的评估标准,并介绍了James Bach的“足够好质量”方法,该方法从产品优势、问题严重性和整体接受度等角度综合评估测试完成度。

24 WHEN CAN WE STOP TESTING?

2015-09-25


THERE IS NO simple way of deciding when a system is completely tested. Most authors agree that there is no single criterion we can use in order to decide that we have fi nished the job. Here are some variants of opinion.

24.1 Five Criteria for Completion of Testing

According to Lee Copeland, there are five elementary criteria which, together, are usually used for decided when you can stop testing. 109 These are when:

  1. We have achieved the coverage aims we defined in the strategy
  2. The number defects discovered is lower than the boundary value we defi ned
  3. The cost of detecting more defects is larger than the estimated loss arising from remaining defects.
  4. The project team draws the collective conclusion that the product is ready to be released.
  5. The decision maker gives the order to go to production.

There is a multitude of weaknesses in these criteria when taken one at a time:

  1. The aim of reaching a certain level of coverage may run the risk of driving the testers towards writing fewer or inferior test cases, simply in order to manage running everything.
  2. Not discovering any more defects may be down to us not testing in the right way, or the best testers being on holiday, but it does not necessarily mean that there are no more defects.
  3. The cost being more than the benefi t of more test cases is a highly subjective evaluation which the tester is certainly not qualifi ed to make: this is more a business issue.
  4. The consensus of the team may be a relatively better measure since, here, we discuss matters with developers, testers and people familiar with the operation together.
  5. The last criterion is a deadline which is decided outside the testers’ domain, and actually has nothing to do with how well we have tested, but rather builds on a pure business judgement that we have to release the product by a certain date.

24.2 Good-Enough Quality

James Bach describes an approach he calls Good-Enough Quality, which he summarises with the following four points which must all be fulfilled:

  1. It has enough benefits
  2. It has no critical problems
  3. The benefits suffi ciently outweigh the drawbacks
  4. In the situation at hand, with all things considered, further testing and improvements would do more harm than good.

To arrive at the above, we can explain each part in a little more detail. Ask the following questions:

  1. Which specific advantages does our product have? How great is the likelihood that one of our target customers will make use of a particular advantage? How important is each advantage? Which parts are critical? Are all advantages good enough for our target customers when taken together?
  2. What potential problems does our product have? How great is the likelihood that one of our target customers will be exposed to a particular problem? How damaging can each problem be? What problems are wholly unacceptable? Are all problems, when considered together, too many for our target customers to be satisfied?
  3. Does the product have enough advantages so that the drawbacks that happen to arise are few enough? How good does the product have to be in order to be accepted by the customers?
  4. In what ways could we improve the product? What would this mean for our costs? Is there the possibility of delivering now, and then delivering improvements later? Precisely what advantages would

转载于:https://www.cnblogs.com/Ming8006/p/4838962.html

/ prepareTracks_l() must be called with ThreadBase::mLock held AudioFlinger::PlaybackThread::mixer_state AudioFlinger::MixerThread::prepareTracks_l( Vector< sp<Track> > *tracksToRemove) { mixer_state mixerStatus = MIXER_IDLE; // find out which tracks need to be processed size_t count = mActiveTracks.size(); size_t mixedTracks = 0; size_t tracksWithEffect = 0; // counts only _active_ fast tracks size_t fastTracks = 0; uint32_t resetMask = 0; // bit mask of fast tracks that need to be reset float masterVolume = mMasterVolume; bool masterMute = mMasterMute; if (masterMute) { masterVolume = 0; } // Delegate master volume control to effect in output mix effect chain if needed sp<EffectChain> chain = getEffectChain_l(AUDIO_SESSION_OUTPUT_MIX); if (chain != 0) { uint32_t v = (uint32_t)(masterVolume * (1 << 24)); chain->setVolume_l(&v, &v); masterVolume = (float)((v + (1 << 23)) >> 24); chain.clear(); } // prepare a new state to push FastMixerStateQueue *sq = NULL; FastMixerState *state = NULL; bool didModify = false; FastMixerStateQueue::block_t block = FastMixerStateQueue::BLOCK_UNTIL_PUSHED; if (mFastMixer != 0) { sq = mFastMixer->sq(); state = sq->begin(); } mMixerBufferValid = false; // mMixerBuffer has no valid data until appropriate tracks found. mEffectBufferValid = false; // mEffectBuffer has no valid data until tracks found. for (size_t i=0 ; i<count ; i++) { const sp<Track> t = mActiveTracks[i].promote(); if (t == 0) { continue; } // this const just means the local variable doesn't change Track* const track = t.get(); // process fast tracks if (track->isFastTrack()) { // It's theoretically possible (though unlikely) for a fast track to be created // and then removed within the same normal mix cycle. This is not a problem, as // the track never becomes active so it's fast mixer slot is never touched. // The converse, of removing an (active) track and then creating a new track // at the identical fast mixer slot within the same normal mix cycle, // is impossible because the slot isn't marked available until the end of each cycle. int j = track->mFastIndex; ALOG_ASSERT(0 < j && j < (int)FastMixerState::sMaxFastTracks); ALOG_ASSERT(!(mFastTrackAvailMask & (1 << j))); FastTrack *fastTrack = &state->mFastTracks[j]; // Determine whether the track is currently in underrun condition, // and whether it had a recent underrun. FastTrackDump *ftDump = &mFastMixerDumpState.mTracks[j]; FastTrackUnderruns underruns = ftDump->mUnderruns; uint32_t recentFull = (underruns.mBitFields.mFull - track->mObservedUnderruns.mBitFields.mFull) & UNDERRUN_MASK; uint32_t recentPartial = (underruns.mBitFields.mPartial - track->mObservedUnderruns.mBitFields.mPartial) & UNDERRUN_MASK; uint32_t recentEmpty = (underruns.mBitFields.mEmpty - track->mObservedUnderruns.mBitFields.mEmpty) & UNDERRUN_MASK; uint32_t recentUnderruns = recentPartial + recentEmpty; track->mObservedUnderruns = underruns; // don't count underruns that occur while stopping or pausing // or stopped which can occur when flush() is called while active if (!(track->isStopping() || track->isPausing() || track->isStopped()) && recentUnderruns > 0) { // FIXME fast mixer will pull & mix partial buffers, but we count as a full underrun track->mAudioTrackServerProxy->tallyUnderrunFrames(recentUnderruns * mFrameCount); } else { track->mAudioTrackServerProxy->tallyUnderrunFrames(0); } // This is similar to the state machine for normal tracks, // with a few modifications for fast tracks. bool isActive = true; switch (track->mState) { case TrackBase::STOPPING_1: // track stays active in STOPPING_1 state until first underrun if (recentUnderruns > 0 || track->isTerminated()) { track->mState = TrackBase::STOPPING_2; } break; case TrackBase::PAUSING: // ramp down is not yet implemented track->setPaused(); break; case TrackBase::RESUMING: // ramp up is not yet implemented track->mState = TrackBase::ACTIVE; break; case TrackBase::ACTIVE: if (recentFull > 0 || recentPartial > 0) { // track has provided at least some frames recently: reset retry count track->mRetryCount = kMaxTrackRetries; } if (recentUnderruns == 0) { // no recent underruns: stay active break; } // there has recently been an underrun of some kind if (track->sharedBuffer() == 0) { // were any of the recent underruns "empty" (no frames available)? if (recentEmpty == 0) { // no, then ignore the partial underruns as they are allowed indefinitely break; } // there has recently been an "empty" underrun: decrement the retry counter if (--(track->mRetryCount) > 0) { break; } // indicate to client process that the track was disabled because of underrun; // it will then automatically call start() when data is available track->disable(); // remove from active list, but state remains ACTIVE [confusing but true] isActive = false; break; } // fall through case TrackBase::STOPPING_2: case TrackBase::PAUSED: case TrackBase::STOPPED: case TrackBase::FLUSHED: // flush() while active // Check for presentation complete if track is inactive // We have consumed all the buffers of this track. // This would be incomplete if we auto-paused on underrun { size_t audioHALFrames = (mOutput->stream->get_latency(mOutput->stream)*mSampleRate) / 1000; int64_t framesWritten = mBytesWritten / mFrameSize; if (!(mStandby || track->presentationComplete(framesWritten, audioHALFrames))) { // track stays in active list until presentation is complete break; } } if (track->isStopping_2()) { track->mState = TrackBase::STOPPED; } if (track->isStopped()) { // Can't reset directly, as fast mixer is still polling this track // track->reset(); // So instead mark this track as needing to be reset after push with ack resetMask |= 1 << i; } isActive = false; break; case TrackBase::IDLE: default: LOG_ALWAYS_FATAL("unexpected track state %d", track->mState); } if (isActive) { // was it previously inactive? if (!(state->mTrackMask & (1 << j))) { ExtendedAudioBufferProvider *eabp = track; VolumeProvider *vp = track; fastTrack->mBufferProvider = eabp; fastTrack->mVolumeProvider = vp; fastTrack->mChannelMask = track->mChannelMask; fastTrack->mFormat = track->mFormat; fastTrack->mGeneration++; state->mTrackMask |= 1 << j; didModify = true; // no acknowledgement required for newly active tracks } // cache the combined master volume and stream type volume for fast mixer; this // lacks any synchronization or barrier so VolumeProvider may read a stale value track->mCachedVolume = masterVolume * mStreamTypes[track->streamType()].volume; ++fastTracks; } else { // was it previously active? if (state->mTrackMask & (1 << j)) { fastTrack->mBufferProvider = NULL; fastTrack->mGeneration++; state->mTrackMask &= ~(1 << j); didModify = true; // If any fast tracks were removed, we must wait for acknowledgement // because we're about to decrement the last sp<> on those tracks. block = FastMixerStateQueue::BLOCK_UNTIL_ACKED; } else { LOG_ALWAYS_FATAL("fast track %d should have been active; " "mState=%d, mTrackMask=%#x, recentUnderruns=%u, isShared=%d", j, track->mState, state->mTrackMask, recentUnderruns, track->sharedBuffer() != 0); } tracksToRemove->add(track); // Avoids a misleading display in dumpsys track->mObservedUnderruns.mBitFields.mMostRecent = UNDERRUN_FULL; } continue; } { // local variable scope to avoid goto warning audio_track_cblk_t* cblk = track->cblk(); // The first time a track is added we wait // for all its buffers to be filled before processing it int name = track->name(); // make sure that we have enough frames to mix one full buffer. // enforce this condition only once to enable draining the buffer in case the client // app does not call stop() and relies on underrun to stop: // hence the test on (mMixerStatus == MIXER_TRACKS_READY) meaning the track was mixed // during last round size_t desiredFrames; const uint32_t sampleRate = track->mAudioTrackServerProxy->getSampleRate(); AudioPlaybackRate playbackRate = track->mAudioTrackServerProxy->getPlaybackRate(); desiredFrames = sourceFramesNeededWithTimestretch( sampleRate, mNormalFrameCount, mSampleRate, playbackRate.mSpeed); // TODO: ONLY USED FOR LEGACY RESAMPLERS, remove when they are removed. // add frames already consumed but not yet released by the resampler // because mAudioTrackServerProxy->framesReady() will include these frames desiredFrames += mAudioMixer->getUnreleasedFrames(track->name()); uint32_t minFrames = 1; if ((track->sharedBuffer() == 0) && !track->isStopped() && !track->isPausing() && (mMixerStatusIgnoringFastTracks == MIXER_TRACKS_READY)) { minFrames = desiredFrames; } size_t framesReady = track->framesReady(); if (ATRACE_ENABLED()) { // I wish we had formatted trace names char traceName[16]; strcpy(traceName, "nRdy"); int name = track->name(); if (AudioMixer::TRACK0 <= name && name < (int) (AudioMixer::TRACK0 + AudioMixer::MAX_NUM_TRACKS)) { name -= AudioMixer::TRACK0; traceName[4] = (name / 10) + '0'; traceName[5] = (name % 10) + '0'; } else { traceName[4] = '?'; traceName[5] = '?'; } traceName[6] = '\0'; ATRACE_INT(traceName, framesReady); } if ((framesReady >= minFrames) && track->isReady() && !track->isPaused() && !track->isTerminated()) { ALOGVV("track %d s=%08x [OK] on thread %p", name, cblk->mServer, this); mixedTracks++; // track->mainBuffer() != mSinkBuffer or mMixerBuffer means // there is an effect chain connected to the track chain.clear(); if (track->mainBuffer() != mSinkBuffer && track->mainBuffer() != mMixerBuffer) { if (mEffectBufferEnabled) { mEffectBufferValid = true; // Later can set directly. } chain = getEffectChain_l(track->sessionId()); // Delegate volume control to effect in track effect chain if needed if (chain != 0) { tracksWithEffect++; } else { ALOGW("prepareTracks_l(): track %d attached to effect but no chain found on " "session %d", name, track->sessionId()); } } int param = AudioMixer::VOLUME; if (track->mFillingUpStatus == Track::FS_FILLED) { // no ramp for the first volume setting track->mFillingUpStatus = Track::FS_ACTIVE; if (track->mState == TrackBase::RESUMING) { track->mState = TrackBase::ACTIVE; param = AudioMixer::RAMP_VOLUME; } mAudioMixer->setParameter(name, AudioMixer::RESAMPLE, AudioMixer::RESET, NULL); // FIXME should not make a decision based on mServer } else if (cblk->mServer != 0) { // If the track is stopped before the first frame was mixed, // do not apply ramp param = AudioMixer::RAMP_VOLUME; } // compute volume for this track uint32_t vl, vr; // in U8.24 integer format float vlf, vrf, vaf; // in [0.0, 1.0] float format if (track->isPausing() || mStreamTypes[track->streamType()].mute) { vl = vr = 0; vlf = vrf = vaf = 0.; if (track->isPausing()) { track->setPaused(); } } else { // read original volumes with volume control float typeVolume = mStreamTypes[track->streamType()].volume; float v = masterVolume * typeVolume; //add for boot video:sync audio for boot char value[PROPERTY_VALUE_MAX] = ""; property_get("persist.sys.bootvideo.enable", value, "false"); if(!strcmp(value,"true")){ property_get("sys.bootvideo.closed", value, "1"); if (atoi(value) == 0){ ALOGV("bootvideo running now,audioflinger no need to control volume"); v = 1.0; } } AudioTrackServerProxy *proxy = track->mAudioTrackServerProxy; gain_minifloat_packed_t vlr = proxy->getVolumeLR(); vlf = float_from_gain(gain_minifloat_unpack_left(vlr)); vrf = float_from_gain(gain_minifloat_unpack_right(vlr)); // track volumes come from shared memory, so can't be trusted and must be clamped if (vlf > GAIN_FLOAT_UNITY) { ALOGV("Track left volume out of range: %.3g", vlf); vlf = GAIN_FLOAT_UNITY; } if (vrf > GAIN_FLOAT_UNITY) { ALOGV("Track right volume out of range: %.3g", vrf); vrf = GAIN_FLOAT_UNITY; } // now apply the master volume and stream type volume vlf *= v; vrf *= v; // assuming master volume and stream type volume each go up to 1.0, // then derive vl and vr as U8.24 versions for the effect chain const float scaleto8_24 = MAX_GAIN_INT * MAX_GAIN_INT; vl = (uint32_t) (scaleto8_24 * vlf); vr = (uint32_t) (scaleto8_24 * vrf); // vl and vr are now in U8.24 format uint16_t sendLevel = proxy->getSendLevel_U4_12(); // send level comes from shared memory and so may be corrupt if (sendLevel > MAX_GAIN_INT) { ALOGV("Track send level out of range: %04X", sendLevel); sendLevel = MAX_GAIN_INT; } // vaf is represented as [0.0, 1.0] float by rescaling sendLevel vaf = v * sendLevel * (1. / MAX_GAIN_INT); } // Delegate volume control to effect in track effect chain if needed if (chain != 0 && chain->setVolume_l(&vl, &vr)) { // Do not ramp volume if volume is controlled by effect param = AudioMixer::VOLUME; // Update remaining floating point volume levels vlf = (float)vl / (1 << 24); vrf = (float)vr / (1 << 24); track->mHasVolumeController = true; } else { // force no volume ramp when volume controller was just disabled or removed // from effect chain to avoid volume spike if (track->mHasVolumeController) { param = AudioMixer::VOLUME; } track->mHasVolumeController = false; } // XXX: these things DON'T need to be done each time mAudioMixer->setBufferProvider(name, track); mAudioMixer->enable(name); mAudioMixer->setParameter(name, param, AudioMixer::VOLUME0, &vlf); mAudioMixer->setParameter(name, param, AudioMixer::VOLUME1, &vrf); mAudioMixer->setParameter(name, param, AudioMixer::AUXLEVEL, &vaf); mAudioMixer->setParameter( name, AudioMixer::TRACK, AudioMixer::FORMAT, (void *)track->format()); mAudioMixer->setParameter( name, AudioMixer::TRACK, AudioMixer::CHANNEL_MASK, (void *)(uintptr_t)track->channelMask()); mAudioMixer->setParameter( name, AudioMixer::TRACK, AudioMixer::MIXER_CHANNEL_MASK, (void *)(uintptr_t)mChannelMask); // limit track sample rate to 2 x output sample rate, which changes at re-configuration uint32_t maxSampleRate = mSampleRate * AUDIO_RESAMPLER_DOWN_RATIO_MAX; uint32_t reqSampleRate = track->mAudioTrackServerProxy->getSampleRate(); if (reqSampleRate == 0) { reqSampleRate = mSampleRate; } else if (reqSampleRate > maxSampleRate) { reqSampleRate = maxSampleRate; } mAudioMixer->setParameter( name, AudioMixer::RESAMPLE, AudioMixer::SAMPLE_RATE, (void *)(uintptr_t)reqSampleRate); AudioPlaybackRate playbackRate = track->mAudioTrackServerProxy->getPlaybackRate(); mAudioMixer->setParameter( name, AudioMixer::TIMESTRETCH, AudioMixer::PLAYBACK_RATE, &playbackRate); /* * Select the appropriate output buffer for the track. * * Tracks with effects go into their own effects chain buffer * and from there into either mEffectBuffer or mSinkBuffer. * * Other tracks can use mMixerBuffer for higher precision * channel accumulation. If this buffer is enabled * (mMixerBufferEnabled true), then selected tracks will accumulate * into it. * */ if (mMixerBufferEnabled && (track->mainBuffer() == mSinkBuffer || track->mainBuffer() == mMixerBuffer)) { mAudioMixer->setParameter( name, AudioMixer::TRACK, AudioMixer::MIXER_FORMAT, (void *)mMixerBufferFormat); mAudioMixer->setParameter( name, AudioMixer::TRACK, AudioMixer::MAIN_BUFFER, (void *)mMixerBuffer); // TODO: override track->mainBuffer()? mMixerBufferValid = true; } else { mAudioMixer->setParameter( name, AudioMixer::TRACK, AudioMixer::MIXER_FORMAT, (void *)AUDIO_FORMAT_PCM_16_BIT); mAudioMixer->setParameter( name, AudioMixer::TRACK, AudioMixer::MAIN_BUFFER, (void *)track->mainBuffer()); } mAudioMixer->setParameter( name, AudioMixer::TRACK, AudioMixer::AUX_BUFFER, (void *)track->auxBuffer()); // reset retry count track->mRetryCount = kMaxTrackRetries; // If one track is ready, set the mixer ready if: // - the mixer was not ready during previous round OR // - no other track is not ready if (mMixerStatusIgnoringFastTracks != MIXER_TRACKS_READY || mixerStatus != MIXER_TRACKS_ENABLED) { mixerStatus = MIXER_TRACKS_READY; } } else { if (framesReady < desiredFrames && !track->isStopped() && !track->isPaused()) { ALOGV("track(%p) underrun, framesReady(%zu) < framesDesired(%zd)", track, framesReady, desiredFrames); track->mAudioTrackServerProxy->tallyUnderrunFrames(desiredFrames); } else { track->mAudioTrackServerProxy->tallyUnderrunFrames(0); } // clear effect chain input buffer if an active track underruns to avoid sending // previous audio buffer again to effects chain = getEffectChain_l(track->sessionId()); if (chain != 0) { chain->clearInputBuffer(); } ALOGVV("track %d s=%08x [NOT READY] on thread %p", name, cblk->mServer, this); if ((track->sharedBuffer() != 0) || track->isTerminated() || track->isStopped() || track->isPaused()) { // We have consumed all the buffers of this track. // Remove it from the list of active tracks. // TODO: use actual buffer filling status instead of latency when available from // audio HAL size_t audioHALFrames = (latency_l() * mSampleRate) / 1000; int64_t framesWritten = mBytesWritten / mFrameSize; if (mStandby || track->presentationComplete(framesWritten, audioHALFrames)) { if (track->isStopped()) { track->reset(); } tracksToRemove->add(track); } } else { // No buffers for this track. Give it a few chances to // fill a buffer, then remove it from active list. if (--(track->mRetryCount) <= 0) { ALOGI("BUFFER TIMEOUT: remove(%d) from active list on thread %p", name, this); tracksToRemove->add(track); // indicate to client process that the track was disabled because of underrun; // it will then automatically call start() when data is available track->disable(); // If one track is not ready, mark the mixer also not ready if: // - the mixer was ready during previous round OR // - no other track is ready } else if (mMixerStatusIgnoringFastTracks == MIXER_TRACKS_READY || mixerStatus != MIXER_TRACKS_READY) { mixerStatus = MIXER_TRACKS_ENABLED; } } mAudioMixer->disable(name); } } // local variable scope to avoid goto warning } // Push the new FastMixer state if necessary bool pauseAudioWatchdog = false; if (didModify) { state->mFastTracksGen++; // if the fast mixer was active, but now there are no fast tracks, then put it in cold idle if (kUseFastMixer == FastMixer_Dynamic && state->mCommand == FastMixerState::MIX_WRITE && state->mTrackMask <= 1) { state->mCommand = FastMixerState::COLD_IDLE; state->mColdFutexAddr = &mFastMixerFutex; state->mColdGen++; mFastMixerFutex = 0; if (kUseFastMixer == FastMixer_Dynamic) { mNormalSink = mOutputSink; } // If we go into cold idle, need to wait for acknowledgement // so that fast mixer stops doing I/O. block = FastMixerStateQueue::BLOCK_UNTIL_ACKED; pauseAudioWatchdog = true; } } if (sq != NULL) { sq->end(didModify); sq->push(block); } #ifdef AUDIO_WATCHDOG if (pauseAudioWatchdog && mAudioWatchdog != 0) { mAudioWatchdog->pause(); } #endif // Now perform the deferred reset on fast tracks that have stopped while (resetMask != 0) { size_t i = __builtin_ctz(resetMask); ALOG_ASSERT(i < count); resetMask &= ~(1 << i); sp<Track> t = mActiveTracks[i].promote(); if (t == 0) { continue; } Track* track = t.get(); ALOG_ASSERT(track->isFastTrack() && track->isStopped()); track->reset(); } // remove all the tracks that need to be... removeTracks_l(*tracksToRemove); if (getEffectChain_l(AUDIO_SESSION_OUTPUT_MIX) != 0) { mEffectBufferValid = true; } if (mEffectBufferValid) { // as long as there are effects we should clear the effects buffer, to avoid // passing a non-clean buffer to the effect chain memset(mEffectBuffer, 0, mEffectBufferSize); } // sink or mix buffer must be cleared if all tracks are connected to an // effect chain as in this case the mixer will not write to the sink or mix buffer // and track effects will accumulate into it if ((mBytesRemaining == 0) && ((mixedTracks != 0 && mixedTracks == tracksWithEffect) || (mixedTracks == 0 && fastTracks > 0))) { // FIXME as a performance optimization, should remember previous zero status if (mMixerBufferValid) { memset(mMixerBuffer, 0, mMixerBufferSize); // TODO: In testing, mSinkBuffer below need not be cleared because // the PlaybackThread::threadLoop() copies mMixerBuffer into mSinkBuffer // after mixing. // // To enforce this guarantee: // ((mixedTracks != 0 && mixedTracks == tracksWithEffect) || // (mixedTracks == 0 && fastTracks > 0)) // must imply MIXER_TRACKS_READY. // Later, we may clear buffers regardless, and skip much of this logic. } // FIXME as a performance optimization, should remember previous zero status memset(mSinkBuffer, 0, mNormalFrameCount * mFrameSize); } // if any fast tracks, then status is ready mMixerStatusIgnoringFastTracks = mixerStatus; if (fastTracks > 0) { mixerStatus = MIXER_TRACKS_READY; } return mixerStatus; } 根据上下文优化这段代码
08-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值